3-Linux Shellcode开发(Stager & Reverse Shell)

文章首发于先知社区:https://xz.aliyun.com/news/17993arrow-up-right 作者:一天

一、环境准备

1.1 工具安装

在linux系统下有几个工具做的很好,比如说pwndbg和GDB Dashboard等GDB 增强工具,但是我就是喜欢用vscode将所有功能集成到一起。对于一个非开发出生的半路型网安业余选手,不能像各位大佬一样直接熟练地运用GDB 命令行调式程序,调试工具对非开发背景的人来说确实有一定门槛,但通过 VS Code 的图形化界面​​ 也能轻松上手。

我看到网上有很多介绍linux x86 shellcode的实现,所以本文只探究x64 shellcode的实现。

首先在kali安装vscode,参考文章:【安装教程】kali 虚拟机下载vscode以及无法启动问题_kali安装vscode-CSDN博客arrow-up-rightDownload Visual Studio Code - Mac, Linux, Windowsarrow-up-right

然后呢,我再安装中文插件,虽然能看懂英文,但毕竟不是母语,老是需要在脑中翻译成中文太累了。

安装C/C++扩展,这个是用来调式程序的

我习惯于在调式程序的时候查看内存的情况,特别是堆栈的情况。在网上找资料的时候发现了一个很好用的插件:MemoryView

效果如下图所示

x86 and x86-64 Assembly 是 VS Code 的一款汇编语言插件,支持语法高亮、代码补全

kali默认安装了GDB调式工具,可以用命令查看一下 gdb -version

kali也默认安装了nasm,用命令查看一下:nasm -v

1.2 运行配置

首先创建一个工作目录,随后用vscode打开,然后在工作目录添加一个hello.asm文件

hello.asm代码如下

配置task.json

  • Ctrl+Shift+P 弹出命令面板

  • 输入 tasks

  • 选择 Tasks: Configure Task... 来针对特定任务进行配置

使用模板创建tasks.json 文件

随便选一个模板

然后用下面的json代码覆盖原有的代码

  • rm -f *.o:表示删除当前工作目录下的所有的*.o文件

  • nasm -f elf64 -g -F dwarf ${file} -o ${fileDirname}/${fileBasenameNoExtension}.o:将汇编源代码文件(.asm)编译生成一个带调试信息的 64 位 ELF 格式目标文件(.o)

  • ld -o ${fileDirname}/${fileBasenameNoExtension} ${fileDirname}/${fileBasenameNoExtension}.o:将汇编生成的 .o 目标文件链接成最终的可执行文件

  • rm -f ${fileDirname}/${fileBasenameNoExtension}.o:再次删除当前工作目录下的*.o文件,确保没有中间产物

一切准备就绪后,我们来到hello.asm界面,按住快捷键:crtl+shift+B 进行快速构建,如果一切顺利,会在当前工作目录下生成一个hello.elf文件

我们运行一下这个hello.elf,在终端输入:./hello

当然编写程序少不了调式环节,我们在.vscode目录下新建一个 lanuch.json 文件,将下面的代码复制到 lanuch.json 文件中。这个代码主要功能就是在当前工作目录中寻找目标程序 (由 ${fileDirname}/${fileBasenameNoExtension} 指定)并进行调试。具体配置就用ai来解释吧。

正常来说vscode只允许特定的文件下断点,为了给我们的asm文件下断点,就需要在设置->调式->勾选Allow Breakpoints Everywhere,这样我们就可以下断点啦。

一切准备就绪后,我们来到hello.asm界面,在 mov rax, 1 ; sys_write 处下一个断点,按住快捷键:F5进行快速调式,如果一切顺利,程序会停在断点处,我们可以查看寄存器的值,也可以查看内存的情况。

二、stager(反向TCP)

Shellcode 的实现通常依赖于系统调用(syscall),因为系统调用是用户空间程序与内核交互的唯一方式并且Shellcode通常需要独立运行,不能假设目标环境中存在 libc 或其他库。系统调用本质上是运行在内核态的特殊函数,windows上也有系统调用,而且从windows系统调用也延伸出重要的防御规避技术。

2.1 调用约定

在 Linux x86-64 架构下,系统调用(syscall)的参数传递遵循 System V AMD64 ABI 调用约定,与用户态函数调用(如 libc 函数)的传参方式一致。

前六个参数从左至右依次存放于 RDI,RSI,RDX,RCX,R8,R9 寄存器里面,剩下的参数通过栈传递,从右至左顺序入栈;

注意

  1. 系统调用号保存在 rax 中。

  2. syscall 指令会覆盖 rcx,因此不能直接用 rcx 传参,而应该使用 R10 来替代 RCX

  3. 因为我们使用的是syscall指令,不用关注rsp对齐,但是使用call调用函数之前,rsp必需对齐!

  4. syscall返回结果如果是负数则表示发生了错误,其值表示错误码的类型

总结:在linux shellcode编程中,我们应该使用 RDI,RSI,RDX,R10,R8,R9,来传递前六个参数。

2.2 分段编写

代码参考msf的源码,系统调用原型参考linux的源码

(1) 调用socket

  1. 可以看到代码中,使用push-pop来设置寄存器的值,理由:①避免00截断(此项可忽略?我看msf生成的shellcode也有00字节);②减少shellcode体积,一般情况下push-pop比mov指令少几个字节。

  2. 需要用 rax 来设置系统调用号,系统调用号参考linux的源码中的 syscall_32.tbl或者syscall_64.tbl

  3. js 用于检查符号标志位(SF),判断结果是否为负数。在linux系统中,当程序通过 syscall 调用内核功能时正数或零:表示成功;负数:表示错误,代表着对于的错误码。

(2) 调用connect

看过我前几篇文章或者有关网络编程相关基础的师傅肯定对 sockaddr_in 这个结构体不陌生,我再简单的说明一下这条指令mov r12,0101A8C05C110002h,其实重点还是 sockaddr_in 结构体如何构造

  • 0101A8C0: C0=192, A8=168, 01=1, 01=1,即ip=192.168.1.1

  • 5C11(大端序):端口4444

  • 0002 :表示AF_INET(IPv4)

为什么结构体大小是16呢?我们不是只设置了8个字节(0101A8C05C110002h),还有一个字段是 char sin_zero[8]; ,必须填充为0,不必显式填充

(3)调用mmap分配一个可执行的缓冲区

  • rdi:addr,映射的起始地址,通常为 0 表示由内核自动选择。

  • rsi:length,映射区域的大小,我设置为0x2000,这个主要是根据stage的大小来设置的。

  • rdx:prot,内存保护标志,我设置为 7 表示内存保护标志的组合,即 PROT_READ(1) | PROT_WRITE(2) | PROT_EXEC(4) =111(2进制)。

  • r10:flags,映射类型和选项,即 MAP_PRIVATE(0x02) | MAP_ANONYMOUS(0x20) =0x22。

  • r8:fd,文件描述符,当 flags 包含 MAP_ANONYMOUS 时,fd 参数会被忽略(通常设为 -10),故不显式设置

  • r9:offset文件偏移量,匿名映射时为 0。

(4)传输stage

关键代码解释

  1. pop rcx:清除之前保存在栈上的sockaddr_in结构体

  2. pop rdi:获取在第一步 调用socket 中保存的socket 句柄

  3. mov rdx, 0x2000 一次性读取完stage,并不能分段读取socket中的数据(可能是我水平有限,等我再研究研究o.0? :)

2.3 测试

首先,我们将 1.2 运行配置 中的hello.asm制作成bin文件,可以用010 editor,也可以用nasm,下面我将介绍使用nasm将单个asm文件生成bin文件。

然后,我们将hello.bin文件放置到python启动的服务器上,代码如下。运行该脚本,服务器会监听自己的4444端口,等待客户端连接

mov rdx, 0x2000 下一个断点,读取数据前的效果如下

执行完syscall指令后,可以看到我们的stage已经在缓冲区了

我们直接执行shellcode.elf,而非调式,效果如下图所示

我们换ubuntu系统来执行shellcode.elf

接下来我们根据shellcode.asm直接生成shellcode.bin

一个简单的linux c语言shellcode加载器

编译成可执行程序

执行shellcode_loader(确保shellcode.bin与shellcode_loader处在同一目录)

2.4 完整代码

三、stager(正向TCP)

正向TCP应该没什么好讲的了,实在是写不出新花样了,看注释应该能明白吧?无非就是socket+bind+listen+accept+read。

3.1 完整代码

3.2 测试

我们来到shellcode.asm界面,按快捷键 ctrl+shift+B 进行快速构建,构建完后,我们执行shellcode.elf

此时程序阻塞,等待连接

python客户端的代码如下,此处是客户端发送hello.bin的数据给服务器(shellcode.asm)

换ubuntu来测试

四、Linux Reverse Shell

在Linux中,反弹Shell(Reverse Shell)是一种常见的技术,通常用于合法渗透测试、远程管理。在这里我将尝试实现反弹shell的shellcode

4.1 值得关注的点

反弹shell的shellcode的方式有多种,我就介绍最常用也最通用的一种方式:通过socket+connect+dup2+execve的系统调用组合

dup2 是一个非常重要的系统调用,常用于 Shellcode 中实现文件描述符的重定向,特别是在反弹 Shell 场景中用于将标准输入(0)、输出(1)和错误(2)重定向到网络套接字。原型如下

当建立一个反弹Shell时,我们需要让远程连接的套接字完全替代标准I/O:

  • 标准输入(stdin, 0):接收攻击者输入的命令

  • 标准输出(stdout, 1):发送命令输出结果给攻击者

  • 标准错误(stderr, 2):发送错误信息给攻击者

没有重定向:

有重定向:

execve系统调用,用于指定的程序替换当前进程的内存空间。在 Shellcode 开发中,execve 常用于启动 shell(如 /bin/sh),原型如下

③又是字符串问题

在linux shellcode开发中,我使用 mov rdi, '/bin/sh' 来定义字符串,然后将字符串压入栈中,如下图所示

在windows上以相同的方式定义字符串,结果却入下图所示

有没有好心的大佬告知其中的缘由啊啊啊啊啊啊啊啊啊啊啊啊!!!!!!!!!!!

1.png

4.2 测试

我的sockaddr_in设置为:0x0101A8C05C110002,即192.168.1.1:4444, AF_INET

当然也可以设置成0x0100007F5C110002,即127.0.0.1:4444, AF_INET,然后在自己的kali上测试。

4.3 完整代码

五、Windows Reverse Shell

在网络安全探索之旅中,我偶然萌生了用汇编实现 Windows 反弹 shell 的想法,实在不想单开一篇文章,就在这里写了。反弹 shell 是一种网络攻防技术,攻击者借助此技术可在目标计算机上获取远程命令行访问权限。常见实现多基于 PowerShell,但汇编语言能深入底层,实现更隐蔽、高效的控制,刚好我这个专题或多或是涉及到汇编语言编写工具,所以Reverse Shell Shellcode孕育而生。

像Linux Reverse Shell一样,windows反弹shell的实现依赖于socket编程,刚好我在前面详细介绍过了socket编程了,咱们成热打铁,一起踏上用MASM汇编实现反弹shell的旅程吧!代码参考,我做了必要的修改,简化了一部分流程。

大致流程如下:

  1. 初始化Winsock库​

  2. 使用WSASocketA函数创建Socket

  3. 使用connect函数连接远程主机​

  4. 创建STARTUPINFOA,重定向标准输入、输出、错误到网络套接字

  5. 创建cmd进程

5.1 值得关注的点

第1到第3步我就不讲了,毕竟已经说过好多次了,咱们的重点应该放在后续重定向标准输入、输出、错误到网络套接字

(1)初始化STARTUPINFOA结构体

首先我们来看STARTUPINFOA结构体结体的定义:

这里我们只用关注以下的几个字段

  1. cb:结构体的大小(字节数),必须初始化为 sizeof(STARTUPINFOA),位于偏移0的位置

  2. dwFlags:控制哪些成员有效,常用 STARTF_USESTDHANDLES 启用标准句柄重定向,位于偏移 4+4(对齐用的)+8*3+4*7=60 的位置

  3. hStdInput:标准输入,位于偏移 4+4(对齐用的)+8*3+4*8+2*2+4(对齐用的)+8=80 的位置

  4. hStdOutput:标准输出,位于偏移 4+4(对齐用的)+8*3+4*8+2*2+4(对齐用的)+8+8=88 的位置

  5. hStdError:标准错误,位于偏移 4+4(对齐用的)+8*3+4*8+2*2+4(对齐用的)+8+8+8=96 的位置

在使用 STARTUPINFOA 结构体前必需进行初始化,下面的代码是自实现memset(&si, 0, sizeof(STARTUPINFOA))

  1. sub rsp,68h:给STARTUPINFOA结构体分配sizeof(STARTUPINFOA)大小的栈空间

  2. mov rdi, rsp:将结构体起始地址存入 rdi,供 stosb 使用

  3. xor rax,rax:将 rax 清零,stosb 会写入 0

  4. mov rcx, 68h:设置循环次数(68 字节)

  5. rep stosbrep重复执行 stosb,直到 ecx 减到 0。stosb:将 al 的值(这里是 0)写入 edi 指向的内存,然后 edi 自动 +1

清零后,我们就可以设置关键字段了

调式看一下清零前的栈空间,调式前请运行nc监听!在 rep stosb 下一个断点

F11后

继续往下调式

si.cb = sizeof(STARTUPINFOA)

si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW

si.hStdInput = socket,socket的句柄保存在r15中,接下来的si.hStdOutput = socket和si.hStdError = socket也是同样的方法调式。

(2)调用CreateProcessA

调用Windows API前需要传入参数,CreateProcessA函数原型如下

其中我们需要关注的参数有

  1. lpCommandLine:若 lpApplicationNameNULL,则从 lpCommandLine 的第一个空格分隔部分解析可执行文件名,说明我们只用指定 lpCommandLine 为“cmd.exe”即可

  2. bInheritHandles:若需跨进程通信(如管道重定向),设为 TRUE

  3. lpStartupInfoSTARTUPINFOA结构体指针

  4. lpProcessInformationPROCESS_INFORMATION 结构体指针

我们来看看用汇编如何实现CreateProcessA的参数传递

需要关注的就是为 PROCESS_INFORMATION 分配空间,执行到 sub rsp, 32 时,虽然我们的RSP已经按照16字节对齐了,但是需要分配24 字节的 PROCESS_INFORMATION ,而且后续有6次push操作,则 rsp要减去 24+6*8=50,执行到call指令时rsp以8结尾,因为不对齐的缘故,程序异常,所以要加上8字节用于对齐,最终rsp减去 24+8(用于对齐)+6*8=56,rsp保持16字节对齐了,即以0结尾。

调式看一下,不管对不对齐,此时的RSP必定以0结尾,首先看不对齐会怎么样。

执行完call指令后,发生异常

对齐后,执行完call指令后,没有发生异常,且rax为非0,这表明CreateProcessA 成功执行。

所以,这也是为什么windows x64 shellcode编写如此困难的原因,因为我们要时刻关注RSP对齐!

5.2 测试

win11上以exe的形式反弹shell

win11以shellcode的形式反弹shell

win10上可以正常反弹shell

win7上可以反弹shell,但是会显示"已停止工作"

5.3 完整代码

Kernel32.dll+CreateProcessA=5DDB71FAh

5.4 往期文章纠错

(1)是先对齐填充,后设置参数!!!!!

Windows Shellcode开发(x64 stager) 文章中,我写的注释是先填参数后对齐填充,虽然我按照 Stephen Fewer 的注释写的,但这是错误的。

我们验证一下,在 10. 调用CreateProcessAcall GetProcAddressByHash 下一个断点

因为执行完 GetProcAddressByHash 函数后,栈空间如下图所示,很明显影子空间后面就是需要通过栈来传递的参数,比如 01 00 00 00 00 00 00 00 就是 bInheritHandles 参数

修改成下面所示的代码

再次调式看看,因为执行完 push rbx 后执行 push 0 ,所以windows API会将0作为bInheritHandles的值

执行后,程序异常

(2)参数注释错误

如说在 Windows Shellcode开发(x86 stager) 文章中出现了不少注释错误,主要原因还是因为我让AI帮我写注释,然后我也没仔细检查。19-05-02-8f7537d564f80495dab5e043a02e3522-20250512190502-6f438e.png

正确的应该为

六、下一步计划

下一篇文章的内容是有些颠覆性的,夸张的说法o.O,但光是想想就兴奋起来了٩(๑˃̵ᴗ˂̵๑)۶。文章已经完成可行性分析和技术验证,大概率会发表,所以srdi的文章要往后推了。如果粉丝数超过15,我会在这个月内发表出来,所以各位师傅的点赞、收藏和关注真的对我很重要呜呜呜呜呜呜>.<

参考资料

[1]: 【安装教程】kali 虚拟机下载vscode以及无法启动问题_kali安装vscode-CSDN博客arrow-up-right [2]: Download Visual Studio Code - Mac, Linux, Windowsarrow-up-right [3]:https://course.ccs.neu.edu/cs3650sp23/l/02/x86-64-sysv-abi.pdf [4]: metasploit-framework/external/source/shellcode/linux/x64/stager_sock_reverse.s at master · rapid7/metasploit-frameworkarrow-up-right [5]: linux/include/linux/syscalls.h at master · torvalds/linuxarrow-up-right [6]: linux/arch/x86/entry/syscalls at master · torvalds/linuxarrow-up-right [7]: Creating a shellcode: Reverse tcp shell | by INMUNE7 | Mediumarrow-up-right [8]: 免杀那点事之随手C写一个持久反弹shell(六)arrow-up-right [9]: STARTUPINFOA (processthreadsapi.h) - Win32 apps | Microsoft Learnarrow-up-right [10]: CreateProcessA 函数 (processthreadsapi.h) - Win32 apps | Microsoft Learnarrow-up-right [11]: Windows Shellcode开发(x64 stager)-先知社区arrow-up-right [12]: Windows Shellcode开发(x86 stager)-先知社区arrow-up-right

Last updated