http://blog.sina.com.cn/s/blog_492101c7010002sy.html
在学习笔记1中,主要分析了一个非常简单的有strcpy函数的程序被溢出的原理,以及相关的栈结构,而且分析的是Windows下的汇编。
现在我们再来分析一段Linux中提供一个shell的shellcode的汇编代码。重点在于分析其中的函数调用。
C程序如下:
//Shellcode.c
#include <stdio.h>
int main(int argc,char* argv[]){
char* name[2];//定义一个2个元素的字符指针数组
name[0]="/bin/sh"; //第一个指针指向一个常量字符串
name[1]=NULL;
execve(name[0],name,NULL); //调用函数execve来执行/bin/sh
}
上面的C程序比较简单,就是调用函数execve来执行/bin/sh
函数execve第一个参数指向要执行程序的完整文件名,
第二个参数指向完整命令行,包括程序完整文件名,也就是我们通常所说的argv数组
第三个指向环境变量块,也是一个字符指针数组
值得注意的是:execve() 调用 成功 后 不会 返回, 其 进程 的 正文(text), 数据(data), bss 和 堆栈(stack) 段 被 调入程序 覆盖. 调入程序 继承了 调用程序 的 PID 和所有 打开的 文件描述符, 他们 不会 因为 exec 过程 而 关闭. 父进程 的 未决 信号被 清除. 所有 被 调用进程 设置过 的 信号 重置为 缺省行为。更多请参考 http://cmpp.linuxforum.net/cman-html/man2/execve.2.html
同时这个网站提供了一个Linux系统函数参考很好的手册。
编译一下程序:
gcc -static -o Shellcode Shellcode.c
这里我们使用了静态编译,以避免动态编译带来的不必要的干扰。
如果我们运行一个这个程序,我们就转移到了一个新的shell:
./Shellcode
sh-2.05$
正如前面说的一样,execve() 调用 成功 后是不会 返回的,因此成功执行execve()后,新的程序会完全接替当前进程,完美附体。
下面我们用gdb调试该程序:
gdb Shellcode
看看gcc为main生成的汇编代码:
(gdb)disas main
;B1
push
mov
;B2
sub
movl
movl
sub
;B3
push
lea
push
pushl 0xfffffff8(%ebp)
call
add
;B4
leave
ret
大概看一下这段代码,我们会发现Linux下的汇编跟Windows下的非常类似。
我把代码大概分成了4块:
B1:
照样是保存上级函数的栈底到它的栈中,然后以上级函数的栈顶作为自己的栈底,开栈
B2:
为局部变量name分配8B空间(两个字符指针)。紧接着就把常量串"/bin/sh"的地址存入name[0],也就是第一个指针,数组的第一个元素;然后把0存入name[1],也就是说这是一个空指针。
需要注意的是linux中的汇编喜欢用加上一个复数的补来表示减去一个数,这也许是为了计算起来简单,因为减法最后还是要转变成加法来运算。不过在一定程度上是以可读性为代价的。
接下来,又在栈里开出了4B,但是我目前还不明白这个到底是为了什么:(
其实本块的前三句完全可以用两个push代替:
pushl 0x0
pushl 0x0808e488
B3:
现在就该进行函数调用了,调用前先把参数压栈。注意压栈顺序是从右到左,也就是说最后一个参数先进栈,这样做的目的是让第一个参数离被调用函数最近。
一开始是一个空指针,也就是说环境块是空的。
然后就是得到name[0]的地址,也就是name的值,然后压栈
再然后就是把name[0]的值压栈,也就是要执行程序的完整文件名
最后就是call了
函数返回后,父函数调整栈指针,把先前压栈的3个参数占的12B以及那个不知名的4B,加起来刚好16B也就是0x10的空间丢弃。
B4:
main函数已经完成,现在该作些收尾工作了
这里的leave就是对:mov %ebp,%esp pop %ebp的封装,其实也就是windows汇编中最后调整堆栈的那两句
我就想,为了对称,何不把开头的push
ret跟windows中的也是一样,从当前栈中pop出返回地址并且返回。
再看看gcc为__execve生成的汇编代码:
disas __execve
;仍然是开栈,不多说了
push
mov
mov
;看看%eax是否是0,为下面的je做准备,但还不清楚到底是什么用处
test
;这两个寄存器下面会用到,因此先把他们的值保存起来,后面用完后再pop出来
push
push
;把第一个参数放到edi,因为ebp的下面4B的old ebp和4B的old eip,再往下才是第一个参数
mov
je
call
mov
mov
push
mov
mov
int
pop
mov
cmp
jbe
neg
call
mov
mov
mov
;恢复ebx,edi寄存器的值
pop
pop
;平衡堆栈并返回
mov
pop
ret
从上面代码中我们首先可以总结出Linux下系统调用是如果发生的:
Linux下系统调用总是通过寄存器来传递参数的,这么做效率比较高,但是却限制了系统调用参数个数,并且也使得参数缺少灵活性。
当有大量参数需要传递时就把它们组装在数据结构中,而只传递数据结构指针。而Windows则通过堆栈传递参数。而如果寄存器中传递的是指针,那么当进入到系统调用后,系统已处于核心态,但是寄存器中所指向的则是用户态的数据,因此需要先用copy_from_user()一类的函数把这些参数从用户空间拷贝到核心态。
并且,eax里放系统调用号,ebx里放第一个参数,ecx放第二个参数,edx放第三个参数。
如果execve()调用失败的话,程序将继续从堆栈中获取指令并执行,而此时堆栈中的数据是随机的,通常这个程序会core dump。我们希望如果execve调用失败的话,程序可以正常退出,因此我们必须在execve调用后准备好当万一execve()执行失败让程序安全退出的代码。