隐藏在图片中的密钥

2019-04-01 23:20:58

在客户端开发的时候,有时需要把密钥保存在本地。这时就会遇到密钥安全性的问题。要保证密钥安全性,无非就是混淆、隐藏、白盒等手段。本文以隐藏在图片中来阐述密钥的安全保存。

PNG图片

便携式网络图形(PNG)是一种无损压缩的位图图形格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。

文件结构

PNG图像格式文件由一个8字节的PNG文件标识域和3个以上的后续数据块(IHDR, IDAT, IEND)组成。

十六进制含义
89用于检测传输系统是否支持8位的字符编码
50 4E 47PNG每个字母对应的ASCII
0D 0ADOS风格的换行符
1A在DOS命令下,用于阻止文件显示的文件结束符
0AUnix风格的换行符

PNG定义了两种类型的数据块:一种是PNG文件必须包含、读写软件也都必须要支持的关键块(critical chunk); 另一种叫做辅助块, PNG允许软件忽略它不认识的附加块。
关键数据块中的4个标准数据块:

  • 文件头数据块IHDR:包含有图像基本信息,作为第一个数据块出现并只出现一次。

  • 调色板数据块PLET:必须放在图像数据块之前。

  • 图像数据块IDAT:存储实际图像数据。PNG数据允许包含多个连续的图像数据块。

  • 图像结束数据IEND:放在文件尾部,表示PNG数据流结束。
    每个数据块都由下表所示的4个域组成

名称字节数说明
length4字节指定数据块中数据域的长度,不超过2^31-1字节
Chunk Type Code(数据块类型)4字节数据块类型码由ASCII字母A-Za-z组成
Chunk Data(数据块数据)可变长度存储按照Chunk Type Code指定的数据
CRC(循环冗余检测)4字节存储用来检测是否有错误的循环冗余码,计算不包括length字段

例子

以下面这张图片为例,使用Hxd工具来看一下实际的数据。

test

我们用Hxd工具打开图片,首先看到的是89 50 4E 47 0D 0A 1A 0A,表示是一张PNG图片。

PNG标识域

紧接着的是IHDR。前面4字节表示长度,长度后面的4个字节是数据块类型,接着是数据块数据,最后4字节是CRC。

IHDR

再接着是PLTE调色板数据块,格式同上。

PLTE

然后我们会看到tEXt数据块,这个是可选数据块,里面可以放入图片的介绍说明,同时我们也可以将密钥放在其中。

tEXt

然后是图像数据块IDAT。

IDAT

最后是图像结束数据IEND。

IEND

CRC算法

CRC是一种根据网络数据包或电脑文件等数据产生简短固定位数校验码的一种散列函数,主要用来检测或校验数据传输或者保存后可能出现的错误。它是校验和的一种,是两个字节数据流采用二进制除法(没有进位,使用XOR来代替减法)相除所得到的余数。
PNG图片中的CRC算法为CRC32。其多项式表示为0x04C11DB7或者0xEDB88320(反转)。另外CRC计算值可以到在线网站比如ip33上计算得到。

下面是用代码实现的CRC32算法:

unsigned int getCrc32(unsigned char* inStr, unsigned int len) {    unsigned int CRC32Table[256];    unsigned int i,j;    unsigned int CRC;    for (i = 0; i < 256; i++) {
        CRC = i;        for (j = 0; j < 8; j++) {            if (CRC & 1)
                CRC = (CRC >> 1) ^ 0xEDB88320;            else
                CRC >>= 1;
        }
        CRC32Table[i] = CRC;
    }
    CRC = 0xffffffff;    for (unsigned int m = 0; m < len; m++) {
        CRC = (CRC >> 8) ^ CRC32Table[(CRC & 0xFF) ^ inStr[m]];
    }
    
    CRC ^= 0xFFFFFFFF;    return CRC;
}

图片中的密钥

tEXt隐藏

在图片中增加密钥,可以在非关键字段比如tEXt中进行。首先要去除原先图片中tEXt字段,然后填充自己要加入的tEXt字段。

int generateNewPng() {
    FILE *fp, *fpnew;    unsigned char *buf = NULL;    unsigned int len = 0;    unsigned int ChunkLen = 0;    unsigned int ChunkCRC32 = 0;    unsigned int ChunkOffset = 0;    unsigned int crc32 = 0;    unsigned int i = 0, j = 0;    unsigned char Signature[8] = {0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a};    unsigned char IEND[12]={0x00,0x00,0x00,0x00,0x49,0x45,0x4e,0x44,0xae,0x42,0x60,0x82};    if ((fp = fopen("oldpng.png", "rb+")) == NULL) {        return 0;
    }    if ((fpnew = fopen("newpng.png", "wb")) == NULL) {        return 0;
    }
    fseek(fp, 0, SEEK_END);     //移动末尾
    len = (int)ftell(fp);       //计算长度
    buf = new unsigned char[len];
    fseek(fp, 0, SEEK_SET);     //移动首
    fread(buf, len, 1, fp);    printf("Total len = %d\n", len);    printf("----------------------------------------------------\n");
    fseek(fp, 8, SEEK_SET);
    ChunkOffset = 8;
    i = 0;
    fwrite(Signature, 8, 1, fpnew);    while (1) {
        i++;
        j=0;        memset(buf, 0, len);    //重置 0
        fread(buf, 4, 1, fp);   //读完文件流位置指针后移size * count。读的是长度
        fwrite(buf, 4, 1, fpnew);   //写入新png
        ChunkLen = (buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
        fread(buf,4+ChunkLen,1,fp);     //读的是数据块类型和内容
        printf("[+]ChunkName:%c%c%c%c        ",buf[0],buf[1],buf[2],buf[3]); //数据
        if(strncmp((char *)buf, "tEXt", 4)==0) {            //过滤掉辅助数据块
            printf("Ancillary Chunk\n");
            fseek(fpnew, -4, SEEK_CUR);
            j = 1;
        } else {            printf("Palette Chunk\n");
            fwrite(buf, 4+ChunkLen, 1, fpnew);
        }        printf("   ChunkOffset:0x%08x    \n",ChunkOffset);        printf("   ChunkLen: %10d        \n",ChunkLen);
        ChunkOffset+=ChunkLen+12;
        crc32=getCrc32(buf,ChunkLen+4);        printf("   ExpectCRC32:%08X\n",crc32);
        fread(buf,4,1,fp);      
        ChunkCRC32=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];        printf("   ChunkCRC32: %08X        ",ChunkCRC32);  
        if(crc32!=ChunkCRC32)            printf("[!]CRC32Check Error!\n");            else {                printf("Check Success!\n\n");                if (j == 0) {
                    fwrite(buf, 4, 1, fpnew);
                }
            }
        ChunkLen=(int)ftell(fp);   //得到当前文件的偏移位置
        if(ChunkLen==(len-12)) {            printf("\n----------------------------------------------------\n");            printf("Total Chunk:%d\n",i);            break;
            
        }
    }    
    char payload[] = "test";    unsigned char *tmpbuf;    int templen;    //    int crc32;
    templen = (int)strlen(payload);
    tmpbuf = new unsigned char[templen+12];
    tmpbuf[0] = templen>>24&0xff;
    tmpbuf[1] = templen>>16&0xff;
    tmpbuf[2] = templen>>8&0xff;
    tmpbuf[3] = templen&0xff;
    tmpbuf[4] = 't';
    tmpbuf[5] = 'E';
    tmpbuf[6] = 'X';
    tmpbuf[7] = 't';    for (int j=0; j<len; j++) {
        buf[j+8] = payload[j];
    }
    buf[len+8] = 0X88;
    buf[len+9] = 0X1E;
    buf[len+10] = 0XE2;
    buf[len+11] = 0X27;
    fwrite(buf, len+12, 1, fp);
    
    fwrite(IEND, 12, 1, fpnew);
    fclose(fp);
    fclose(fpnew);    return 0;
}

填充完所要的数据后,接着在程序中可以解析新图片中tEXt字段。

int getPayload(const char *res) {
    FILE *fp;    unsigned char *buf = NULL;    unsigned int len = 0;    if ((fp = fopen("newpng.png", "rb+")) == NULL) {        return 0;
    }
    fseek(fp, 0, SEEK_END);     //移动末尾
    len = (int)ftell(fp);       //计算长度
    buf = new unsigned char[len];
    fseek(fp, 0, SEEK_SET);     //移动首
    fread(buf, len, 1, fp);
    printf("Total len = %d\n", len);
    printf("----------------------------------------------------\n");    
    for(int i=1;i<=len;i++) {
        printf("%02X ",buf[i-1]);        if(i%16==0)
            printf("\n");
    }    const char *text = "tEXt";
    std::string s((const char *)buf,len);
    size_t start = s.find(text);    const char *iend = "IEND";
    size_t end = s.find(iend);
    std::string result = s.substr(start+4, end-start-12);
    res = result.c_str();
    printf("----------------------------------------------------\n");
    printf("%s\n",result.c_str());
    fclose(fp);
    printf("\n");    return 0;
}

LBS隐藏

LBS隐藏是一种更加隐秘的隐藏手段。它通过改写IDAT数据中的RGB三通道数据的低3位,把密钥藏进去。因为只改写了低位数据,所以人眼往往很难区分出来。具体的实现可以参考cloacked-pixel。由于每个像素点最多隐藏3位,就会导致一个量的问题。当隐藏的数据比较多时,就会需要比较大的图片。

参考

PNG
CRC
隐写技巧——利用PNG文件格式隐藏Payload



  • 2018-04-18 15:56:00

    linux下如何实现mysql数据库每天自动备份定时备份

    备份是容灾的基础,是指为防止系统出现操作失误或系统故障导致数据丢失,而将全部或部分数据集合从应用主机的硬盘或阵列复制到其它的存储介质的过程。而对于一些网站、系统来说,数据库就是一切,所以做好数据库的备份是至关重要的!

  • 2018-04-18 20:44:19

    $(...).live is not a function

    jquery中的live()方法在jquery1.9及以上的版本中已被废弃了,如果使用,会抛出TypeError: $(...).live is not a function错误。

  • 2018-04-19 16:31:03

    mysql双机热备的实现

    准备两个mysql,A和B,A为主,B为从。前提是这两个数据库现在的表结构要一模一样,否则不成功。这个要锁表处理了。

  • 2018-04-19 16:32:47

    mysql binlog_do_db参数设置的坑

    在配置文件中想当然地配置成binlog_do_db=test,xx,jj,以为是三个库。结果无论什么操作都没有binlog产生

  • 2018-04-20 02:11:58

    Android中finish掉其它的Activity

    在Android开发时,一般情况下我们如果需要关掉当前Activity非常容易,只需要一行代码 this.finish;即可。 那么,如果是想要在当前Activity中关掉其它的Activity应该怎么实现呢? 比如更改了某个设定,程序需要重新运行并加载新的配置文件,就要用到restart或finish让程序重启。

  • 2018-04-20 09:12:07

    如何在 7 分钟内黑掉 40 家网站?

    去年夏天我开始学习信息安全与黑客技术。在过去的一年中,我通过参加各种战争游戏、夺旗以及渗透测试模拟,不断提高我的黑客技术,还学习了很多关于“如何让计算机偏离其预期行为”的新技术。

  • 2018-04-25 00:46:48

    Android开发笔记——SharedPreferences 存储实体类以及任意类型

    我们常常要用到保存数据,Android中常用的存储方式有SQLite,sharedPreferences 等,当然也有各自的应用场景,前者适用于保存较多数据的情形,后者责倾向于保存用户偏好设置比如某个checkbox的选择状态,用户登录的状态等等,都是以键值对的形式进行的文件读取,可以存储String,int,booean等一些基本数据类型等等。

  • 2018-04-25 11:48:44

    Java泛型详解

    泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除。