3-Linux Shellcode开发(Stager & Reverse Shell)
文章首发于先知社区:https://xz.aliyun.com/news/17993 作者:一天
一、环境准备
1.1 工具安装
在linux系统下有几个工具做的很好,比如说pwndbg和GDB Dashboard等GDB 增强工具,但是我就是喜欢用vscode将所有功能集成到一起。对于一个非开发出生的半路型网安业余选手,不能像各位大佬一样直接熟练地运用GDB 命令行调式程序,调试工具对非开发背景的人来说确实有一定门槛,但通过 VS Code 的图形化界面 也能轻松上手。
我看到网上有很多介绍linux x86 shellcode的实现,所以本文只探究x64 shellcode的实现。
首先在kali安装vscode,参考文章:【安装教程】kali 虚拟机下载vscode以及无法启动问题_kali安装vscode-CSDN博客 和Download Visual Studio Code - Mac, Linux, Windows
然后呢,我再安装中文插件,虽然能看懂英文,但毕竟不是母语,老是需要在脑中翻译成中文太累了。

安装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 寄存器里面,剩下的参数通过栈传递,从右至左顺序入栈;
⚠注意:
系统调用号保存在
rax中。syscall指令会覆盖rcx,因此不能直接用rcx传参,而应该使用R10来替代RCX。因为我们使用的是
syscall指令,不用关注rsp对齐,但是使用call调用函数之前,rsp必需对齐!syscall返回结果如果是负数则表示发生了错误,其值表示错误码的类型
总结:在linux shellcode编程中,我们应该使用 RDI,RSI,RDX,R10,R8,R9,来传递前六个参数。

2.2 分段编写
代码参考msf的源码,系统调用原型参考linux的源码
(1) 调用socket
可以看到代码中,使用push-pop来设置寄存器的值,理由:①避免00截断(此项可忽略?我看msf生成的shellcode也有00字节);②减少shellcode体积,一般情况下push-pop比mov指令少几个字节。
需要用
rax来设置系统调用号,系统调用号参考linux的源码中的syscall_32.tbl或者syscall_64.tbljs用于检查符号标志位(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.15C11(大端序):端口44440002:表示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参数会被忽略(通常设为-1或0),故不显式设置r9:offset文件偏移量,匿名映射时为 0。
(4)传输stage
关键代码解释
pop rcx:清除之前保存在栈上的sockaddr_in结构体pop rdi:获取在第一步调用socket中保存的socket 句柄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上以相同的方式定义字符串,结果却入下图所示

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

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的旅程吧!代码参考,我做了必要的修改,简化了一部分流程。
大致流程如下:
初始化Winsock库
使用WSASocketA函数创建Socket
使用connect函数连接远程主机
创建STARTUPINFOA,重定向标准输入、输出、错误到网络套接字
创建cmd进程
5.1 值得关注的点
第1到第3步我就不讲了,毕竟已经说过好多次了,咱们的重点应该放在后续重定向标准输入、输出、错误到网络套接字
(1)初始化STARTUPINFOA结构体
首先我们来看STARTUPINFOA结构体结体的定义:
这里我们只用关注以下的几个字段
cb:结构体的大小(字节数),必须初始化为sizeof(STARTUPINFOA),位于偏移0的位置
dwFlags:控制哪些成员有效,常用STARTF_USESTDHANDLES启用标准句柄重定向,位于偏移4+4(对齐用的)+8*3+4*7=60的位置hStdInput:标准输入,位于偏移4+4(对齐用的)+8*3+4*8+2*2+4(对齐用的)+8=80的位置hStdOutput:标准输出,位于偏移4+4(对齐用的)+8*3+4*8+2*2+4(对齐用的)+8+8=88的位置hStdError:标准错误,位于偏移4+4(对齐用的)+8*3+4*8+2*2+4(对齐用的)+8+8+8=96的位置
在使用 STARTUPINFOA 结构体前必需进行初始化,下面的代码是自实现memset(&si, 0, sizeof(STARTUPINFOA))
sub rsp,68h:给STARTUPINFOA结构体分配sizeof(STARTUPINFOA)大小的栈空间mov rdi, rsp:将结构体起始地址存入rdi,供stosb使用xor rax,rax:将rax清零,stosb会写入0。mov rcx, 68h:设置循环次数(68 字节)rep stosb:rep重复执行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函数原型如下
其中我们需要关注的参数有
lpCommandLine:若lpApplicationName为NULL,则从lpCommandLine的第一个空格分隔部分解析可执行文件名,说明我们只用指定lpCommandLine为“cmd.exe”即可bInheritHandles:若需跨进程通信(如管道重定向),设为TRUElpStartupInfo:STARTUPINFOA结构体指针lpProcessInformation:PROCESS_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. 调用CreateProcessA 的 call 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帮我写注释,然后我也没仔细检查。
正确的应该为
六、下一步计划
下一篇文章的内容是有些颠覆性的,夸张的说法o.O,但光是想想就兴奋起来了٩(๑˃̵ᴗ˂̵๑)۶。文章已经完成可行性分析和技术验证,大概率会发表,所以srdi的文章要往后推了。如果粉丝数超过15,我会在这个月内发表出来,所以各位师傅的点赞、收藏和关注真的对我很重要呜呜呜呜呜呜>.<

参考资料
[1]: 【安装教程】kali 虚拟机下载vscode以及无法启动问题_kali安装vscode-CSDN博客 [2]: Download Visual Studio Code - Mac, Linux, Windows [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-framework [5]: linux/include/linux/syscalls.h at master · torvalds/linux [6]: linux/arch/x86/entry/syscalls at master · torvalds/linux [7]: Creating a shellcode: Reverse tcp shell | by INMUNE7 | Medium [8]: 免杀那点事之随手C写一个持久反弹shell(六) [9]: STARTUPINFOA (processthreadsapi.h) - Win32 apps | Microsoft Learn [10]: CreateProcessA 函数 (processthreadsapi.h) - Win32 apps | Microsoft Learn [11]: Windows Shellcode开发(x64 stager)-先知社区 [12]: Windows Shellcode开发(x86 stager)-先知社区
Last updated