隐藏在图片中的密钥

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

PNG图片

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

文件结构

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

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

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

  • 文件头数据块IHDR:包含有图像基本信息,作为第一个数据块出现并只出现一次。
  • 调色板数据块PLET:必须放在图像数据块之前。
  • 图像数据块IDAT:存储实际图像数据。PNG数据允许包含多个连续的图像数据块。
  • 图像结束数据IEND:放在文件尾部,表示PNG数据流结束。
    每个数据块都由下表所示的4个域组成
名称 字节数 说明
length 4字节 指定数据块中数据域的长度,不超过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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,753评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,668评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,090评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,010评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,054评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,806评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,484评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,380评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,873评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,021评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,158评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,838评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,499评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,044评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,159评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,449评论 3 374
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,136评论 2 356

推荐阅读更多精彩内容