はじめに
ある日、私はファイルを連結したらどうなるんだろうという好奇心に逆らえず、おもむろに連結して確かめてみることにしました。
結果、その連結したファイルは普通にファイルとして使えることがわかりました。
ファイルを読み込むシステムによるとは思いますが、後ろのファイルはただ無視されます。
これを利用すればファイルをファイルで隠すことができると思い立ち、ちょっとしたツールを書いてみました。
隠したファイルを取り出す
ファイルの末尾にファイルを書き込めばファイルを隠せることが分かったので、次は取り出す方法を考えてみます。
まあ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ファイルは隠していることが悟られないように、ランダムにカラフルな四角と丸が入るようにし、進んで開きたくないようにしています。