もくじ
問題
この問題は以下のような問題でした。
Hello expert. My PC sent @yuki_kashiwaba’s twitter icon for C&C server. But I couldn’t find any suspicious point. Could you investigate this?
Format: HimitsukichiCTF{XXXX} File: flag.png
問題ファイルとして提供している画像(flag.png)は以下でした。
また、私のTwitterプロフィールからダウンロード可能な画像(rUFTyqG400x400.png)は以下でした。
コンセプト
この問題は「画像ファイルの各ピクセルのRGB値がごくわずかに変化しても、人間は目視で画像の変化に気づくことはできない」という性質を悪用し、各ピクセルのRGB値の末尾1bitを改ざんすることで任意のテキストを埋め込むステガノグラフィ、LSB Steganographyをテーマにしています。
1ピクセルごとに3bit(Alphaも使えば4bit)の値を埋め込むことが可能なので、例えば400*400の画像の場合は、480,000bitを埋め込むことができます。
これは、8bitで表現するASCII文字を使用する場合、60,000文字まで埋め込める想定になります。
このLSB Steganographyですが、手軽な手法の割に入門向けCTFではあまり見かけない気がしたので、今回自分で実装してみました。
Writeup
問題文から、Twitterのプロフィール画像がC&Cサーバに送信されたようだが不審な点が見当たらないので調査してほしいとの依頼を受けた(設定である)ことがわかります。
そのため、まずは実際に送信されたflag.png
と、Twitterのプロフィール画面から取得可能な_rUFTyqG_400x400.png
を比較するところからスタートします。
念のためfileコマンドでファイルタイプを確認の上、stringsコマンドで不審な文字列が埋め込まれていないことを確認します。
続いて、exiftoolの出力結果を比較してみますが、こちらもFlagに関する情報はありませんでした。
また、PNG形式なのでbinwalkを試してみますが有益な情報が埋め込まれている様子はありませんでした。
$ binwalk -e flag.png
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 PNG image, 399 x 399, 8-bit/color RGB, non-interlaced
54 0x36 Zlib compressed data, default compression
427 0x1AB Zlib compressed data, default compression
電子透かしなどは目視では確認できず色調などを変えても特に有効な情報は見当たりませんでした。
画像比較のためにmagickのcompositeを使いますが、出力は真っ黒でした。
compositeは色の差分を取るので、見た目同じなら真っ黒になるのは当然ですね。。
$ ./magick composite -compose difference flag.png _rUFTyqG_400x400.png diff.png
compositeでは差分の有無が確認できなかったため、WinMergeを使ってみます。
すると、以下の通り画像のほぼ全箇所に差分があることがわかりました。
ちなみにですが、一部のピクセルにのみ差分がある場合、WinMergeは以下のように差分箇所を表示してくれます。
ここで、各ピクセルのどこに差分があるかについては、WinMerge上の画像をマウスホバーした際にウィンドウの下に表示される情報から確認できます。
ここにはそれぞれの画像の同じ座標のRGBA値が表示されていますが、RGBの値のいずれかに若干の差異があることがわかります。
複数の座標で同じ情報を確認すると、RGBのいずれかに1の差分が発生していることがわかります。
ここで、問題バイナリはC&Cに送信された画像のため、この差分には何らかの情報が埋め込まれていると想定されます。
各RGBの値の差分はいずれも1のみであることから、LSB Steganographyのテクニックが使用されていると推察できます。
というわけで実際にStegsolveのExtract Previewを使用したところ、0bit目の値でFlagが取得できることがわかりました。
ちなみにStegsolveは以下の手順で使用できます。
$ wget http://www.caesum.com/handbook/Stegsolve.jar -O stegsolve.jar
$ chmod +x stegsolve.jar
$ java -jar stegsolve.jar
ピクセルの差分からLSB Steganographyを思いつくにはもしかしたらある程度の知識か検索能力がいるかもしれませんが、実のところ何もわからなくてもStegsolveをぶん回すだけでも解けてしまいます。
作問に使用したスクリプト
Flagの埋め込みは以下のスクリプトで行いました。
from PIL import Image
import struct
def toggle_rmb(b):
if bin(b)[-1] == "1":
return b-1
else:
return b+1
# Init flag binary
flag_binary = ""
with open("_rUFTyqG_400x400.txt", "r") as f_file:
flag = f_file.read()
for f in flag:
flag_binary += "{:08b}".format(ord(f))
# print(flag_binary)
# Load image
image = Image.open("flag.png")
pixel = image.load()
# Get size
img_width = image.width
img_height = image.height
# PUT last bit for each RGB
p = 0
for i in range(img_width):
for j in range(img_height):
r = pixel[j,i][0]
g = pixel[j,i][1]
b = pixel[j,i][2]
if not (flag_binary[p%len(flag_binary)] == bin(r)[-1]):
r = toggle_rmb(r)
p += 1
if not (flag_binary[p%len(flag_binary)] == bin(g)[-1]):
g = toggle_rmb(g)
p += 1
if not (flag_binary[p%len(flag_binary)] == bin(b)[-1]):
b = toggle_rmb(b)
p += 1
image.putpixel((j,i), (r,g,b))
image.save("flag.png")
Stegsolveを使わない解法としては、以下のようなSolverを想定しています。
from PIL import Image
import re
import struct
# Load image
image = Image.open("./flag.png")
pixel = image.load()
# Get size
img_width = image.width
img_height = image.height
# Enumerate pixel RGB bytes
result = ""
for i in range(img_width):
for j in range(img_height):
result += bin(pixel[j,i][0])[-1]
result += bin(pixel[j,i][1])[-1]
result += bin(pixel[j,i][2])[-1]
result_txt = ""
for i in range(0, len(result), 8):
tmp = int("0b"+result[i:i + 8], 2)
result_txt += chr(tmp)
pattern = r'.*?(HimitsukichiCTF{.+?}).*?'
result = re.match(pattern, result_txt)
if result:
print(result.group())
これで、HimitsukichiCTF{I_know_you_can_not_notice_for_this_image_is_already_tampered_by_me}
というFlagを取得できました。
まとめ
手軽な手法の割に入門向けCTFではあまり見かけないLSB Steganographyを実装してみました。