《网络渗透技术》学习笔记(2)——一段简单的汇编代码分析 zz

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  %ebp
mov   %ebp,%esp

;B2
sub    $0x8,%esp
movl  $0x0808e488,0xfffffff8(%ebp)
movl  $0x0,0xfffffffc(%ebp)
sub    $0x4,%esp

;B3
push  %0x0
lea     0xfffffff8(%ebp),%eax
push  %eax
pushl 0xfffffff8(%ebp)
call     0x0804cab0 <__execvc>
add    $0x10,%esp

;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  %ebp mov   %ebp,%esp 也顺便封装成比如enter呢?

ret跟windows中的也是一样,从当前栈中pop出返回地址并且返回。

再看看gcc为__execve生成的汇编代码:
disas __execve

;仍然是开栈,不多说了
push  %ebp
mov   $0x0,%eax      ;0-->eax
mov   %esp,%ebp

;看看%eax是否是0,为下面的je做准备,但还不清楚到底是什么用处
test   %eax,%eax

;这两个寄存器下面会用到,因此先把他们的值保存起来,后面用完后再pop出来
push  %edi
push  %ebx

;把第一个参数放到edi,因为ebp的下面4B的old ebp和4B的old eip,再往下才是第一个参数
mov   0x8(%ebp),%edi
je       __execve+22            ;不清楚这个je目的何如?
call     0x0                    ;这个call就更邪门了
mov   0xc(%ebp),%ecx            ;__execve+22。把第二个参数放ecx
mov   0x10(%ebp),%edx           ;把第三个参数放edx

push  %ebx                ;保存ebx的值
mov   %edi,$ebx           ;把第一个参数放ebx
mov   $0xb,%eax           ;eax里放入execve的系统调用号11
int      $0x80            ;进行系统调用。若execve执行成功,则本程序也会从此一去不复返了,一旦返回也表明execve失败
pop    %ebx               ;恢复ebx的值

mov    %eax,%ebx          ;将execve的返回值存入ebx
cmp    $0xfffff000,%ebx   ;这个比较不明白
jbe     __execve+63      
neg    %ebx
call     __errno_location ;找到errno的存储地址
mov   %ebx,(%eax)         ;把错误号存到errno
mov   $0xffffffff,%ebx    ;-1-->ebx,返回-1表示出错
mov   %ebx,%eax           ;__execve+63

;恢复ebx,edi寄存器的值
pop   %ebx
pop   $edi

;平衡堆栈并返回
mov   %ebp,%esp
pop   %ebp
ret


从上面代码中我们首先可以总结出Linux下系统调用是如果发生的:
Linux下系统调用总是通过寄存器来传递参数的,这么做效率比较高,但是却限制了系统调用参数个数,并且也使得参数缺少灵活性。
当有大量参数需要传递时就把它们组装在数据结构中,而只传递数据结构指针。而Windows则通过堆栈传递参数。而如果寄存器中传递的是指针,那么当进入到系统调用后,系统已处于核心态,但是寄存器中所指向的则是用户态的数据,因此需要先用copy_from_user()一类的函数把这些参数从用户空间拷贝到核心态。
并且,eax里放系统调用号,ebx里放第一个参数,ecx放第二个参数,edx放第三个参数。

如果execve()调用失败的话,程序将继续从堆栈中获取指令并执行,而此时堆栈中的数据是随机的,通常这个程序会core dump。我们希望如果execve调用失败的话,程序可以正常退出,因此我们必须在execve调用后准备好当万一execve()执行失败让程序安全退出的代码。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值