2008年4月18日金曜日

Python: 続・簡単ステガノグラフィ…画像を画像に埋め込む

このブログ記事をはてなブックマークに追加

前回のなんちゃってステガノグラフィはあまりにも酷い出来だと思ったので、少しばかり改良してみた。画像データのアルファ値を利用していたのをRGBに置き換え、最下位ビットを利用する方法に変更した。アルファ値だと生データを見れば一発で怪しいと分かるが、RGBだと余程注意深くないと分からないだろう。また、入力データにはテキストだけではなく、バイナリファイルも指定できるようにした。ファイル名は保存されるので、隠したデータを取り出す際もファイル名の指定が不要だ。

使い方は前回と一緒。ただし、上記の理由で出力ファイルの指定はない。

で、上のネコの画像(kitten01.png)に下のモルモットの画像(guinea01.jpg)を隠してみる。

covgraph.py kitten01.png kitten01g.png guinea01.jpg

画像を隠し入れた画像(kitten01g.png)を下に示す。最初の画像と見た目はほとんど変わらない。

取り出すときは以下のようにする。

covgraph.py kitten01g.png

ステガノグラフィについての知識なんてほとんど持ち合わせていないので、これがどの程度有効かなんて分からないし、きっと世の中にはもっと効率の良いアルゴリズムなんかがあるのだと思うけど、気にしない。だってお遊びのプログラミングだから。作る過程でいろいろ考えることが楽しいのだ。

追記: 出力画像ファイルにはPNGかBMPを利用するのが良い。JPEGは非可逆圧縮により埋め込むデータが壊れるし、GIFはパレットの制限に引っ掛かるためだ。因みに、入力画像ファイルはどのフォーマットでも問題ないはず。また、このサイトはアップロードした画像を非可逆圧縮するので本記事の画像をそのまま利用してもデータを取り出すことはできません。

以下、ソース(covgraph.py)。

#!/usr/bin/env python # -*- coding: utf-8 -*- """covgraph.py by nox, 2008.4.17""" import sys, os, bz2, zlib from PIL import Image def embed_data(data, in_data, offset=0): for i in range(len(in_data)): for j in range(8): n = (i + offset) * 8 + j data[n] = chr((ord(data[n]) & ~1) | (ord(in_data[i]) >> j & 1)) return offset + len(in_data) def cover(input_image, output_image, hidden_data): """データを画像ファイルに埋め込む. input_image: 入力画像ファイル. output_image: 出力画像ファイル. hidden_data: 埋め込むデータもしくはデータファイル. """ filename = zlib.compress("") if os.path.isfile(hidden_data): filename = zlib.compress(hidden_data) hidden_data = file(hidden_data, "rb").read() bz2_data = bz2.compress(hidden_data) im = Image.open(input_image).convert("RGB") data = list(im.tostring()) size = 5 + 1 + len(filename) + len(bz2_data) if len(data) < size * 8 + 1: print >>sys.stderr, "Error: input data too large." sys.exit(1) offset = embed_data(data, "".join([chr((size >> 8 * i) & 0xff) for i in range(5)])) # length = 5 offset = embed_data(data, chr(len(filename)), offset) # length = 1 offset = embed_data(data, filename, offset) # length = len(filename) offset = embed_data(data, bz2_data, offset) # length = len(bz2_data) im.fromstring("".join(data)) im.save(output_image) def bin_to_string(data): return "".join([chr(sum([data[i+j] << j for j in range(8)])) for i in range(0, len(data), 8)]) def uncover(image_file): """画像ファイルからデータを抜き出す. image_file: データが埋め込まれた画像ファイル. 戻り値: ファイル名, データ. """ im = Image.open(image_file) im_data = list(im.tostring()) size = sum([ord(x) << 8 * i for i, x in enumerate(bin_to_string([ord(d) & 1 for d in im_data[:5*8]]))]) name_len = ord(bin_to_string([ord(d) & 1 for d in im_data[5*8:6*8]])) filename = zlib.decompress(bin_to_string([ord(d) & 1 for d in im_data[6*8:(name_len+6)*8]])) bz2_data = bin_to_string([ord(d) & 1 for d in im_data[(name_len+6)*8:size*8]]) hidden_data = bz2.decompress(bz2_data) return filename, hidden_data def main(args): if len(args) < 2 or len(args) > 4: print >>sys.stderr, u"画像ファイルにデータを埋め込む:" print >>sys.stderr, u"Usage: %s <input image> <output image> <data or data-file>" % os.path.basename(args[0]) print >>sys.stderr, u"input image: 入力画像ファイル" print >>sys.stderr, u"output image: 出力画像ファイル" print >>sys.stderr, u"data: 埋め込むデータ" print >>sys.stderr, u"data-file: 埋め込むデータファイル" print >>sys.stderr print >>sys.stderr, u"画像ファイルからデータを抜き出す:" print >>sys.stderr, u"Usage: %s <input image>" % os.path.basename(args[0]) print >>sys.stderr, u"input image: 入力画像ファイル" sys.exit(1) # 画像ファイルにデータを埋め込む. if len(args) == 4: cover(args[1], args[2], args[3]) # データが埋め込まれた画像からデータを取り出す. if len(args) < 4: filename, data = uncover(args[1]) if filename: # データをファイルに書き出す. print "Filename: %s" % filename file(filename, "wb").write(data) else: # データを標準出力に書き出す. print data if __name__ == "__main__": main(sys.argv)

3 コメント:

匿名 さんのコメント...

唐衣着つつ慣れにしつましあればはるばる来ぬる旅をしぞ思ほゆ

nox さんのコメント...

幸運が来ますように…

nox さんのコメント...

ヒントは花言葉。