from:http://blog.donews.com/quickmouse/archive/2007/07/16/1187051.aspx
by quickmouse <quickmouse@263.net> 2007年7月16日
已经有不少的文章提到过Linux系统库函数劫持的方法,最主要的是利用LD_PRELOAD环境变量进行预加载,将自己写的库函数编译成.so以后,将其列入LD_PRELOAD当中,于是程序就会使用我们自己所写的系统库函数。当然,这样做的目的往往是为了在调用系统功能的时候搞点小动作,最终还是要转到真正的库函数里去的。
不过最近在完成一个函数库的劫持过程中,却碰到了一些意外——那就是碰到了我们可爱的可变参数库函数。所谓可变参数库函数,但凡打算看懂这篇文章的人都了解,我就不再仔细解释。最著名的可变参数库函数就是printf了。这回我碰到的是ioctl,也是一个底层常用的库函数,我只需要在ioctl进行某个操作时对它进行劫持。
在我准备动手进行ioctl的劫持之前,我丝毫没有意识到可变参数函数对劫持技巧提出的要求。由于劫持操作之后还需要返回到原系统库函数,于是在传参的时候我发现需要对传入的参数进行解析,以便正确处理。可是ioctl这样牛x的函数完全依靠前面的固定参数来决定是否存在变参,以及它们的类型。这么说我不是要把所有的ioctl请求都解析一遍?(除非我疯了)。
考虑传参的具体实现,是在上一级调用的时候按顺序由右到左依次把参数压入栈,也就是固定参数一定是在最靠近返回地址的位置,而远离它的有多少是参数是天知道的(god knows)。因此,想通过栈信息而不解析参数数值想直接得到参数个数,是行不通的
唯一可行的办法是在跳转到真正的(被劫持的)系统库函数时,保持上一级函数调用劫持函数时的栈信息,绕过变参解析的过程。具体的函数实现框架为:
int ioctl(int d, unsigned long int request, …)
{
if(request != WE_WANT_HIJACK )
{
__asm__ __volatile__(
"\n\
mov 0xfffffffc(%%ebp), %%edi\n\
mov 0xfffffff8(%%ebp), %%esi\n\
mov 0xfffffff4(%%ebp), %%ebx\n\
leave\n\
jmp *%0\n\
"
:
:"m"(real_ioctl));
}
else
{
// The hijack action code here
}
}
当遇到不是我们需要劫持的系统调用方式时,直接使用汇编代码恢复各寄存器的数值,跳转到真实的ioctl函数入口。否则,进入我们自己所写的劫持代码当中。由于我们只劫持了ioctl当中一种(或者几种)请求,这些请求所需要的参数个数、类型是很容易知道的,这就避免了解析所有ioctl请求的情况。
需要注意的地方在如下这一段汇编代码里:
__asm__ __volatile__(
"\n\
mov 0xfffffffc(%%ebp), %%edi\n\
mov 0xfffffff8(%%ebp), %%esi\n\
mov 0xfffffff4(%%ebp), %%ebx\n\
leave\n\
jmp *%0\n\
"
:
:"m"(real_ioctl));
汇编填写的内容并不是完全固定的,需要根据编译出来的库文件,用objdump反汇编以后,作出对应的修改,方法是找出进入这一段汇编代码之前寄存器改变的情况,对ebx,esi,edi等作为“被调用者保存”的寄存器进行恢复,而ebp,esp等基址、栈址在leave指令上恢复(需要esp指向正确)。最后在jmp的指令下直接跳转到真实的ioctl函数入口。real_ioctl可以通过dlsym(RTLD_NEXT, "ioctl")获得。
当然,这一类的劫持并不是万能的,对于使用了setuid/setsid的应用程序,LD_PRELOAD变量并不会起作用。
对于这样的情况,用什么方法再研究研究,呵呵,技术无止境嘛~~