LLVM PASS PWN (二)
这是LLVM PASS PWN的第二篇,这一篇主要记录学习一下LLVM PASS PWN如何调试,并拿CISCN 2021 SATool来做第一道LLVM PASS PWN解,笔者的做题环境都是ubuntu20.04
之前的文章链接:
LLVM PASS PWN (二)
调试 LLVM PASS
在正式做LLVM PASS PWN的时候首先学习一下如何调试LLVM PASS,还是用上一篇文章的LLVM PASS
其实真正攻击的目标是opt,上一章使用的opt-12,所以这里使用gdb /bin/opt-12
,用set args
设置参数传入即可
开始的时候vmmap可以发现并没有加载LLVMHello.so
需要将这些call都跑掉之后,就可以看到LLVMHello.so加载成功
如下加载成功
如果想要在模块中下断点只需要用so第一个的地址加上偏移即可
CISCN 2021 SATool
逆向分析
下载之后可以发现有如下东西
1 | ➜ 附件 ls |
SAPass.so就是自定义的一个Pass,漏洞就发生在这里面,还有一个opt,这个就是我们最后的攻击目标./opt --version
即可查看版本
1 | ➜ 附件 ./opt --version |
还有一个quickstart.txt
,这个是出题人写的说明,看一下
1 | Jack has developed a malicout analysis tool and leave a backdoor in it. |
上面说明了如何操作,并贴出了打远程的脚本,接下来做的就是对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
1 | //clang-8 -S -emit-llvm exp.c -o exp.ll |
用上面调试LLVM PASS的方法对这题进行调试,并在SAPass.so的基址 + 0x1A14
这里下个断点然后单步执行进行调试,同时在调试的时候结合静态分析,if ( v3 == 8 && *Name == 'r0oDkc4B' )
这个判断已经成功满足,但是最后会跑飞跑到0x2234这里就退出了
造成这样的原因是B4ckDo0r
这个函数已经结束了,没有检测到这个函数里面的东西,那我们给他放入一点东西看一下会不会跑到0x2234这里就退出
1 |
|
调试之后发现已经跑不到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继续调试
1 |
|
可以进入save函数处理功能,但是到了if ( -1431655765 * (unsigned int)((v15 + 24 * v18 - 24 * (unsigned __int64)NumTotalBundleOperands - v20) >> 3) == 2 )
这里之后会直接退出了,会判断是否为2,直接盲猜一下是否是save需要两个参数
1 |
|
成功继续执行了,接着到了核心的地方,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里了,调试一下看一下是否正确
1 | pwndbg> x/gx 0x7ffff3b72000 + 0x204100 |
成功执行了,继续看fakekey这个功能
第482行和上面一样,需要传一个参数,所以我们可以构造如下exp.c
1 |
|
调试了之后发现sextvalue是fakekey的一参,但是给了c之后会直接到else这里,所以我们将c改成整数再试试
1 |
|
再调试一下可以发现byte_204100和*heap_ptr这里的值都加上0x10了
1 | pwndbg> x/gx 0x7ffff3d76100 |
继续下一个功能
run这里会直接执行(*heap_ptr)()
漏洞利用
总结一下上面的操作
- save,会将一参二参放入heap_ptr中
- takeaway,
heap_ptr = (_QWORD *)heap_ptr[2];
- stealkey,会将save的一参放入
byte_204100
- fakekey,会将
byte_204100
和*heap_ptr
加上fakekey
的一参 - run,会执行
*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
1 | 0xe3afe execve("/bin/sh", r15, r12) |
1 | 0x7ffff3e39000 0x7ffff3e5b000 r--p 22000 0 /usr/lib/x86_64-linux-gnu/libc-2.31.so |
所以有
1 | pwndbg> p/x 0x7ffff4025bf0 - 0x7ffff3e39000 |
0x1ecbf0 - 0xe3afe = 0x1090f2
,exp如下
1 |
|
总结
如何快速的筛选出有用的代码是最关键的地方,必要的时候需要动静结合分析这样可以快速理解程序逻辑