InternetBot/Stenloader
GitHub: InternetBot/Stenloader
一个用于将shellcode隐藏在BMP图像中的隐写术加载器,演示载荷混淆技术。
Stars: 10 | Forks: 4
# 隐写术 Shellcode 加载器
这个项目演示了如何将有效载荷隐藏在 BMP 文件内。
我是在完成 Maldev Academy 的前几个模块后制作了这个项目。他们的 `.rsrc` 段模块展示了如何将有效载荷存储在 PE 资源中,这让我想起了大学时参加 CTF 竞赛的日子,那时候 flag 总是藏在图像的元数据里。我做了一些研究,意识到我可以直接将有效载荷写入 `.bmp` 文件。我敢肯定这可以适用于任何图像格式,但在这个项目中,我专注于位图图像。
## 工作原理

## XOR 加密
XOR 加密非常简单。同一个函数既可以加密也可以解密 shellcode,用同一个密钥应用两次 XOR 就能恢复其原始值。在我们的例子中,密钥是 `0xBD`。密钥可以更改,但我坚持使用 `0xBD`,因为它的特性将在后面说明时更有意义。另外,可能应该避免使用 `0xFE` 作为你的密钥,因为在嵌入过程中它被用来清除 LSB(最低有效位)。
## BMP 文件
BMP 文件有 3 个部分,但我们只关心第三个部分,即像素数据。这三个部分分别是:文件头、DIB 头和像素数据。
参考: https://engineering.purdue.edu/ece264/16au/hw/HW13
在代码中的某个地方,你会看到我引用了文件头中的像素数据偏移量。原因是像素数据偏移量存储在文件头的第 10 字节处。所以在第 10 字节处,它基本上是在说“嘿,像素数据从第 54 字节开始”,然后代码直接跳转到那里。请查看下面的代码片段。
```
DWORD dwPixelOffset = *(DWORD*)(pBmpBuffer + 10);
// dwPixelOffset = 54
```
## LSB 隐写术
现在到了有趣的部分。LSB 隐写术将数据隐藏在每个像素字节的最低有效位中。记住,一个像素字节由 8 位组成,所以改变最后一位会产生一个肉眼完全不可见的变化。假设我们有一个像素字节是这样的:

我们可以将最低有效位从 1 改为 0,图像在视觉上不会有任何变化。

## 嵌入一个位
还记得我之前强烈不建议使用 `0xFE` 作为密钥吗?那是因为我们要用它来清除 LSB,以便将其设置为 0。
你可能想知道为什么我们不能像这样直接替换成 LSB:
```
lsb[i] = 1
```
那行不通,因为在 C 语言中没有直接访问和替换单个位的方法。所以我们必须使用 AND 和 OR 门进行一些二进制操作。我本应在计算机体系结构课上更专心一点,但我花了很多时间才弄明白这一点。
所以第一步是清除 LSB,将其设置为 0,我们可以使用 AND 操作来完成。`0xFE` 相当于 `1 1 1 1 1 1 1 0`,它将保持我们整个像素字节不变,只将 LSB 改为 0。这看起来可能有点困惑,但这是分解说明:
```
0xFE = 1 1 1 1 1 1 1 0
Pixel = 1 0 1 1 0 1 0 1
AND = 1 0 1 1 0 1 0 0 ← only the LSB changed to 0
Example 2:
0xFE = 1 1 1 1 1 1 1 0
Pixel = 1 0 0 0 1 1 1 1
AND = 1 0 0 0 1 1 1 0 ← only the LSB changed to 0
```

如你在两个示例中所见,除了 LSB 总是被强制设为 0 之外,每一位都保持完全不变。这就是 `0xFE` 的全部意义所在,它是一个只精确清除最后一位的掩码,不影响其他任何东西。
一旦位被清除,我们就可以使用 OR 将我们的 LSB 设置为数据位:
```
Example 1:
Cleared: 1 0 1 1 0 1 0 0 ← LSB cleared to 0
Your bit (1): 0 0 0 0 0 0 0 1 ← data bit we want to embed (1)
OR result: 1 0 1 1 0 1 0 1 ← LSB is now set to our bit
Example 2:
Cleared: 1 0 0 0 1 1 1 0 ← LSB cleared to 0
Your bit (0): 0 0 0 0 0 0 0 0 ← data bit we want to embed (0)
OR result: 1 0 0 0 1 1 1 0 ← LSB stays 0
```

如你所见,当我们的位是 1 时,OR 操作将 LSB 设为 1。当我们的位是 0 时,LSB 保持为 0。像素字节中的其他所有部分都完全不受影响。
## 容量计算
你可能遇到的一个常见问题是你的 shellcode 太大,无法装入 BMP。图像的每个像素字节承载我们 shellcode 的 1 位,而 shellcode 的每个字节总共需要 8 位。所以 1 字节的 shellcode 需要占用 8 个像素字节。
对于我们 276 字节的 shellcode:
276 字节 × 8 位 = 2208 个像素字节(最小值)
因此,你的 BMP 文件在头部之后至少需要有 2208 个可用的像素字节来容纳 shellcode。图像越大,你拥有的空间就越多。
## 提取位
就像我之前说的,没有办法直接访问单个位,比如 `byte.bit[7]`,这在 C 语言中根本不存在。所以在这个场景中,我们将使用一种叫做位移的技术。位移将目标位移动到可以使用 `& 1` 将其分离的最后位置。
把它想象成滑动窗口算法。
https://www.geeksforgeeks.org/dsa/window-sliding-technique/
你将所有位向右滑动,直到目标位到达位置 0,即最后一个位置。然后你用 `& 1`(即 `0 0 0 0 0 0 0 1`)掩码掉其他所有东西,只保留最后一位。
AND 在这里起作用,因为任何与 1 进行 AND 操作的位都会保持其原来的值,而任何与 0 进行 AND 操作的位都会被清除:
```
1 AND 1 = 1 ← kept
0 AND 1 = 0 ← kept
1 AND 0 = 0 ← wiped
0 AND 0 = 0 ← wiped
```
现在我们来取 shellcode 的前两个字节 `0xFC` 和 `0x48`,并逐步演示位移过程。它看起来应该是这样的:


注意向右移动 7 位如何将第 7 位推到了位置 0,我们可以在这里读取它。向右移动 6 位将第 6 位推到位置 0,依此类推。然后 `& 1` 清除了所有随之而来的多余位,只留下我们想要的位。
0xFC 和 0x48 是我们 calc shellcode 的前两个字节,所以这些字节在嵌入过程中会经历这个过程。
没有位移,就无法将一个字节分解成单个位。而没有单个位,你就无法一次将一个位嵌入到像素的 LSB 中。整个隐写术技术都依赖于此。
## 将位重组回一个字节
在提取过程中,每个位使用左移和 OR 操作被放回其正确位置。这与上面类似,只是用左移和 OR。
```
Start: bReassembled = 0 0 0 0 0 0 0 0
bit 7 = 1: 1 << 7 = 1 0 0 0 0 0 0 0
OR result 1 0 0 0 0 0 0 0
bit 6 = 1: 1 << 6 = 0 1 0 0 0 0 0 0
OR result 1 1 0 0 0 0 0 0
bit 5 = 1: 1 << 5 = 0 0 1 0 0 0 0 0
OR result 1 1 1 0 0 0 0 0
... continues until ...
Final: 1 1 1 1 1 1 0 0 = 0xFC
```
## 完整嵌入流程

## 项目结构
```
stenloader/
├── enc.c ← embedder (XOR encrypt + LSB embed into BMP)
├── readrun.c ← loader (extract + decrypt + execute)
├── snail.bmp ← original BMP
└── sten.bmp ← output BMP with hidden payload
```
## 免责声明
这是一个在隔离的虚拟机实验环境中构建的学习项目。
代码比较粗糙,注释是为个人参考而写的。
不要期望代码写得整洁。
这个项目严格出于教育目的而构建,旨在理解隐写术和有效载荷混淆技术在底层是如何工作的。
此代码不得用于任何恶意或非法活动。如果你将此用于在受控实验环境学习之外的其他用途,后果自负,与我无关。
标签:BMP文件处理, CTF竞赛, LSB隐写, meg, PE文件资源, XOR加密技术, 中高交互蜜罐, 二进制分析, 云安全运维, 位图图像处理, 信息安全, 加密算法, 图像文件操作, 客户端加密, 恶意payload, 恶意软件开发, 数据隐藏, 最低有效位, 漏洞利用技术, 网络安全, 隐写术, 隐私保护