第一次尝试写一个从缓冲区溢出攻击的例子,费尽周折总算成功了。虽然简单,但仍然是自己花费了一些时间,仔细研究的结果。下面是代码注入后程序运行的结果。
先把代码写出来,以下代码在MS-DOS 7.10,Turbo C 2.0环境下编译,连接,执行,并有效。
char shellcode[] = "\xb2\x48\x02\xcd\x21\xb2\x51\xb4\x02\xcd\x21\xb4\x4c\xcd\x21";
void foo()
{
int *ret;
ret = (int *)&ret+2;
(*ret ) = (int)sc;
}
void main()
{
foo();
}
shellcode按字面翻译,就是可以交互执行的代码。在这里,是我在一段正常的程序中注入一段我们自己写的代码。要让正常的程序按照我们的意思做事,这就是shellcode的根本目的。
在上面的程序中,字符数组shellcode[]就是存放我们要注入的代码的地方,乍看全是奇怪的符号, 其实都是我要执行的代码的机器码的十六进制形式。执行的效果是:接管程序执行流程,在屏幕上打印出“HQ”两个字符,并正常结束程序。
缓冲区溢出涉及到函数的压栈顺序,也就是栈帧的形成过程。在程序中,一般对于一个具有如下形式的函数调用 void fun(int a,int b),压栈的顺序是,先从右至左将函数的参数压栈,然后是父函数返回地址,父函数栈顶指针的值,最后还要为为函数内局部变量预留空间。那么对于上面的程序,栈帧就是下面这个样子的。
代码里的 ret = (int *)&ret+2 这一句的作用是找到存放返回地址RET的内存地址,接下来 (*ret ) = (int)sc 就是用我们的ShellCode的地址去覆盖父函数的返回地址,那么,当函数结束,准备回到父函数时,就会将ShellCode的首地址(POP)弹到EIP里,那么程序后来就会执行ShellCode的指令了,就会按照自己预先设定好的指令做事。
预想是这个样子的,在Turbo Debugger下调试,确实跳转到了ShellCode字符数组的地址,但是反汇编出来的指令和我们预设的大相径庭。我原来以为机器代码抄错了,但是重新用NASM反汇编以后发现没错。那会是哪里出的问题?
仔细调试以后,才发现程序跳转到的地方是CS:[shellcode],即代码段寄存器CS与shellcode数组偏移共同确定的地址,这个地址位于代码段的范围内,而在程序的初始化时shellcode数组是位于数据段内,那么我们的shellcode的地址是DS:[shellcode]。
在TC编译器默认情况下,采用的是small的编译方式,这意味着代码段和存放数据的段位于不同的内存空间里。另外还有一种更小的编译方式,tiny模式,那么所有的段的首地址都会从同一地址空间开始。
在采用这种编译方式后,在TD下调试,果然跳到了我想要的地址,也就执行了我们想要的代码,如下图,
左上部分的大框里,白色的代码就是我的shellcode,右边的寄存器的查看框里,可以看到,DS,ES,SS和CS这几个段寄存器的值都是6384H,就是CS和DS的值,决定了我的代码注入的成功与否。继续运行,也就得到了文章开头第一幅图的结果。
关键问题出在DS和CS的值的问题,如果我们能让CS和DS的值相等,那么代码注入就不困难了。而一般的程序运行在Ring3,修改CS是Ring0的特权,我们又要进入Ring0。所以剩下的问题就转化成以下两个:
① 在执行代码进入Ring0的前提下,修改CS的值,使CS和存放shellcode的段寄存器值相等。
② 在CS和存放shellcode的段寄存器值相等的前提下,执行我们的shellcode。
一般来说,正常的程序只能获得Ring3的特权级别,而要获得Ring0的只有操作系统的底层服务。那上面的两个问题就变成了,两者互以对方的目的为前提,以自己的前提为另者的目的。按照离散里面的术语,这样就构成一个环了,是没有解的。
现在的程序都不会以tiny模式编译,那么,shellcode的实际意义又在哪里呢?看来还要继续学习。
^_^
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!