本文来分析几道题目,熟悉下LLVM pass pwn的解题流程。
CISCN-2021 satool
国赛题,2022年也出了一道名字都一样的题,可见国赛还是很重视这类题型的。
这一题的附件很难找,笔者将其放在了自己的github中,便于读者复现。
首先当然是使用IDA打开。发现符号表被抠了,不过我们还是有定位runOnFunction的方法。
需要注意的是,题目中给的runOnFunction函数是覆写的llvm库中的runOnFunction方法,因此其一定会被vtable表引用,而vtable表在.rodata段中,只需要调到IDA-view界面查看名字未知的函数中哪一个有足够的长度,而且被.rodata段引用即可。

发现红框中的两个函数都有足够的长度,但是只有sub_19D0被.rodata段引用,因此判定这就是覆写的runOnFunction方法了。
由于我们有llvm的所有头文件源码,因此对于.so文件中llvm方法的调用,我们都可以找到相应的声明,有助于我们理解代码本身。
1. 逆向分析

首先进行的是两个条件判断,这里判断rdx是否为8,函数的返回值是否等于该字符串的值。经过调试发现,上面的函数getName()的返回值是llvm::value对象的名字。需要注意的是,llvm::value是llvm中最重要的一个类,其可以代表一个常量、变量、指令或函数。这里可以看到如果对象的名字不是B4ckDo0r,则会直接退出不作处理。要想进行下面的处理,我们就必须要让一个函数的名字为B4ckDo0r。至于前面对rdx的判断,这应该也算是getName()函数的返回值,其表示的是对象名字的长度。显而易见,B4ckDo0r的长度为8,因此这里判断长度是否为8合情合理。
注:这里说明一下llvm pass类题目如何调试。
我们需要通过opt程序加载.so链接库,同时输入.ll文件以生成IR输出,因此应该调试opt这个程序(带参数的opt调试方式:gdb opt、r 参数, 参数,...),我们可以先对opt下一个在main开头的断点,保证其在一开始执行就能够中断,注意此时.so链接库并未被加载到内存中,本题的opt程序中在main函数有一堆函数调用,快速步过后经过某一条call指令可以发现.so库被加载,此时再下链接库的断点,继续执行就可以让程序断在我们想要调试的runOnFunction()函数了。这种方式是笔者自己探索出来的,比较麻烦,但由于这方面的资料实在太少,找不到现成的参考,只能先这样做了。如有更加简便的方法还请读者不吝赐教。
前面我们知道了只有名为B4ckDo0r的函数才能被优化,后面的代码错综复杂,手动地去一步步分析显然是不太现实的,我们重点关注一下程序中提到的字符串,看能不能找到一些流程控制的线索。
(line 175) if ( !(unsigned int)str_compare(&p_dest, "save") )
(line 300) if ( (unsigned int)str_compare(&p_dest, "takeaway") )
(line 444) if ( !(unsigned int)str_compare(&p_dest, "stealkey") )
(line 469) if ( !(unsigned int)str_compare(&p_dest, "fakekey") )
(line 517) if ( !(unsigned int)str_compare(&p_dest, "fakekey") )
我们很容易能够找到上面的几行语句,这里的save、takeaway等字符串显然是突破的关键所在。
经过调试发现,这里的&p_dest指的是函数中操作符的名字,如第一条语句调用printf函数时,操作符名即为printf。因此我们不仅要保证其函数名为B4ckDo0r,还要保证其中调用的函数名为上面五个字符串中的一个。
在save控制流程中,我们发现了这些代码,通过字符串可以猜测出这应该是报错信息,对于使用C语言程序正常生成的.ll文件,应该不会有这样的问题出现。因此在下面凡是看到跳转到这些label的代码,就统统都可以不看了,能够节省很多时间。

剔除掉这些检查的代码之后,我们发现真正对我们有用的代码实际上也就这么几行。
// save
v31 = n;
v32 = malloc(0x18uLL);
v32[2] = byte_2040f8;
byte_2040f8 = v32;
v33 = (char *)src;
memcpy(v32, src, v31);
v34 = v32 + 1;
v35 = (char *)v84[0];
memcpy(v34, v84[0], (size_t)v84[1]);
if ( v35 != &v85 )
{
operator delete(v35);
v33 = (char *)src;
}
if ( v33 != v88 )
operator delete(v33);
// stealkey
v66 = llvm::CallBase::getNumTotalBundleOperands((llvm::CallBase *)(v6 - 24));
if ( byte_2040f8
&& !(-1431655765
* (unsigned int)((v15
+ 24 * v65
- 24LL * v66
- (v8
- 24 * (unsigned __int64)(*(_DWORD *)(v8 + 20) & 0xFFFFFFF))) >> 3)) )
{
byte_204100 = *byte_2040f8;
}
// fakekey
v76 = byte_204100;
if ( *(_BYTE *)(*(_QWORD *)v75 + 16LL) == 13 )
SExtValue = llvm::APInt::getSExtValue((llvm::APInt *)(*(_QWORD *)v75 + 24LL));

本文详细介绍了CISCN-2021 satool题目解题过程,涉及LLVM pass的逆向分析和调试。通过IDA分析,找到关键函数runOnFunction,发现对函数名及调用的操作符有特定要求。通过调试和修改源代码,满足条件并通过层层检查,最终找到关键操作,控制函数指针执行one_gadget,实现getshell。该过程锻炼了逆向C++程序的能力,对理解和利用LLVM pass具有指导意义。
最低0.47元/天 解锁文章
770

被折叠的 条评论
为什么被折叠?



