任意のファイルをPNGファイルで隠してみる


はじめに

ある日、私はファイルを連結したらどうなるんだろうという好奇心に逆らえず、おもむろに連結して確かめてみることにしました。
結果、その連結したファイルは普通にファイルとして使えることがわかりました。
ファイルを読み込むシステムによるとは思いますが、後ろのファイルはただ無視されます。
これを利用すればファイルをファイルで隠すことができると思い立ち、ちょっとしたツールを書いてみました。

隠したファイルを取り出す

ファイルの末尾にファイルを書き込めばファイルを隠せることが分かったので、次は取り出す方法を考えてみます。
まあ1つ目のファイルの末尾が分かればそこからEOFまで読み込めば良いだけですね。
矢面に立たせるファイルは構造が単純なものが良さそうです。
なおかつ適当に生成したファイルが存在しても不自然ではなさそう画像ファイルが適してそうです。
なので、PNGにしようと思います。

PNGの構造

PNGの末尾を知るためにはPNGについて知らないとどうにもならないので調べてみます。
https://www.w3.org/TR/PNG/
上記を読むにPNGはシグネチャを除いてチャンクというデータの集まりが連なった構造をしているようです。
シグネチャは8バイトでこのファイルがPNGであることを示し10進数で以下のようなデータになっています。
137, 80, 78, 71, 13, 10, 26, 10
チャンクは全部で18種類ありその中でもIHDR、PLTE、IDAT、IENDはcritical chunksと呼ばれ必須なチャンクなようです。
チャンクの構造は以下のようになっています。

1 2 3 4
Length Chunk Type Chunk Data CRC

あるいは

1 2 3
Length(=0) Chunk Type CRC
名前 説明
Length Chunk Dataのバイト数を示す。4バイト。符号なし整数。0から2^31-1の値。
Chunk Type チャンクの種類を示す。4バイト。10進数で65から90および97から122しか使えない。つまり、a-Z。
Chunk Data 実データ。ない場合もある。
CRC (Cyclic Redundancy Code) データの破損をチェックするために使用。Chunk TypeとChunk Dataで計算されてる。4バイト。Chunk Dataがなくても常に存在する。

ファイルが隠れてるか調べてみる

最後のチャンクのChunk Typeは必ずIENDなのでそこまでPNGの構造に従って読み込んでいき、そこがEOFか確認すればファイルが隠れているかわかりそうです。
以下のようなコードでPNGファイルの後ろにまだデータがあるか調べることができます。

package main

import (
  "log"
  "os"
)

type png struct {
  file *os.File
}

// Length渡してChunk Dataの長さを知る
func (p png) getChunkLength(bytes []byte) int {
  return int(bytes[0])*256*256*256*256 + int(bytes[1])*256*256 + int(bytes[2])*256 + int(bytes[3])
}

// シグネチャ見てPNGか調べる
func (p *png) isPng() (bool, error) {
  header, err := p.read(8)
  if err != nil {
    return false, err
  }
  signature := [8]int{137, 80, 78, 71, 13, 10, 26, 10}
  for i := 0; i < len(signature); i++ {
    if int(header[i]) != signature[i] {
      return false, nil
    }
  }
  return true, nil
}

// 末尾まで読み込んでそこがEOFか調べる
func (p *png) isEOF() (bool, error) {
  overflowingData := make([]byte, 1)
  if n, _ := p.file.Read(overflowingData); n != 0 {
    _, err := p.file.Seek(-int64(n), os.SEEK_CUR)
    return true, err
  }
  return false, nil
}

func (p *png) read(length int) ([]byte, error) {
  bytes := make([]byte, length)
  if _, err := p.file.Read(bytes); err != nil {
    return bytes, err
  }
  return bytes, nil
}

func (p *png) isHidden() (bool, error) {
  if isPng, err := p.isPng(); err != nil {
    return false, err
  } else if !isPng {
    return false, nil
  }
  for {
    // Lengthを読む
    length, err := p.read(4)
    if err != nil {
      return false, err
    }

    // Chunk Typeを読む
    type, err := p.read(4)
    if err != nil {
      return false, err
    }

    if string(type) == "IEND" {
      // CRCを読む
      if _, err := p.read(4); err != nil {
        return false, err
      }
      if isEOF, err := p.isEOF(); err != nil {
        return false, err
      } else if isEOF {
        return true, nil
      }
      return false, nil
    }
    // Chunk DataとCRCを読む
    if _, err := p.read(p.getChunkLength(length) + 4); err != nil {
      return false, err
    }
  }
}

func main() {
  file, err := os.Open(os.Args[1])
  if err != nil {
    log.Fatalln(err)
  }
  defer func() {
    if err := file.Close(); err != nil {
      log.Fatalln(err)
    }
  }()

  png := png{file: file}
  log.Println(png.isHidden())
}

成果

https://github.com/atsuya0/hidf
拡張子も埋め込み、取り出したときに.pngからもとの拡張子に戻るようにしてます。
使い方は以下です。

ちなみに生成するPNGファイルは隠していることが悟られないように、ランダムにカラフルな四角と丸が入るようにし、進んで開きたくないようにしています。

この画像をダウンロードしてstringsコマンドを実行すれば文字が後ろに隠れていることが分かります。