All Articles

Himitsukichi CTF Forensic Oblivious

もくじ

問題

この問題は以下のような問題でした。

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)は以下でした。

https://raw.githubusercontent.com/kash1064/Kaeru-no-Himitsukichi/pages/file/flag.png

また、私のTwitterプロフィールからダウンロード可能な画像(rUFTyqG400x400.png)は以下でした。

https://pbs.twimg.com/profile_images/1578007666281938944/original.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

電子透かしなどは目視では確認できず色調などを変えても特に有効な情報は見当たりませんでした。

image-20221006203756806

画像比較のためにmagickのcompositeを使いますが、出力は真っ黒でした。

compositeは色の差分を取るので、見た目同じなら真っ黒になるのは当然ですね。。

$ ./magick composite -compose difference flag.png _rUFTyqG_400x400.png diff.png

compositeでは差分の有無が確認できなかったため、WinMergeを使ってみます。

すると、以下の通り画像のほぼ全箇所に差分があることがわかりました。

image-20221006222431056

ちなみにですが、一部のピクセルにのみ差分がある場合、WinMergeは以下のように差分箇所を表示してくれます。

image-20221006222331173

ここで、各ピクセルのどこに差分があるかについては、WinMerge上の画像をマウスホバーした際にウィンドウの下に表示される情報から確認できます。

ここにはそれぞれの画像の同じ座標のRGBA値が表示されていますが、RGBの値のいずれかに若干の差異があることがわかります。

image-20221006222758863

複数の座標で同じ情報を確認すると、RGBのいずれかに1の差分が発生していることがわかります。

ここで、問題バイナリはC&Cに送信された画像のため、この差分には何らかの情報が埋め込まれていると想定されます。

各RGBの値の差分はいずれも1のみであることから、LSB Steganographyのテクニックが使用されていると推察できます。

参考:PNG - CTF Wiki EN

というわけで実際にStegsolveのExtract Previewを使用したところ、0bit目の値でFlagが取得できることがわかりました。

image-20221006224206197

ちなみに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を実装してみました。