エンディアンに気をつけよう

先日、社内のCTFに参加して、バイトオーダーに考えが及ばなかったばかりに初級の問題を落とす羽目になり、悔しい思いをしたのでメモ。

IDAの逆アセンブル画面のスクリーンキャプチャを渡されるので、逆アセンブルの内容を読んでFlagを読み取るという問題。実際のバイナリは渡されないので、デバッグしながらブレークポイントをセットして、スタックからFlagを読み出す、というようなことはできない。

以下はコードの抜粋。


mov eax, [ebp+argv]
mov edx, [eax+4]
mov ds:somedata, 08CCE97ABh
mov ds:somedata+4, 0B9A08CCEh
mov ds:somedata+8, 0FF989E93h
mov eax, 0

loc_8048480:
not byte ptr ds:somedata[eax]
add eax, 1
cmp eax, 0Ch
jnz short loc_8048480

mov dword ptr [esp+4], offset somedata
mov [esp], edx
call _strcmp
test eax, eax
jz short loc_80484B0

※Flagの値はオリジナルの問題から変更してあります。

DSレジスタに値を格納した後、ループに入り、DSレジスタの先頭から値を1バイトずつ取り出し、NOT演算を行う。(0ビットを1ビットに、1ビットを0ビットに反転する。ビット反転)
NOT演算が完了したら、反転されたDSレジスタの値とコマンドライン引数の値をstrcmpで比較する。

「DSレジスタの初期値は。。。0x8CCE97ABB9A08CCEFF989E93か。この初期値をビット反転させればFlagが取れるんだな」

Flag奪取のため、以下のスクリプトを書いた。


#!/usr/bin/env python

# ds 0x8CCE97ABB9A08CCEFF989E93

word = []

word.append(~0x8c & 0xff)
word.append(~0xce & 0xff)
word.append(~0x97 & 0xff)
word.append(~0xab & 0xff)

word.append(~0xb9 & 0xff)
word.append(~0xa0 & 0xff)
word.append(~0x8c & 0xff)
word.append(~0xce & 0xff)

word.append(~0xff & 0xff)
word.append(~0x98 & 0xff)
word.append(~0x9e & 0xff)
word.append(~0x93 & 0xff)

print(word)

dec = ''

for i in word:

    dec += chr(i)

print(dec)

実行結果。


$ ./solver.py
[115, 49, 104, 84, 70, 95, 115, 49, 0, 103, 97, 108]
s1hTF_s1gal

しかし、s1hTF_s1galをFlagとして入力したところ、弾かれてしまった。

「あれ、なんかコードの読み間違いがあったかな?それともスクリプトに問題があるのかな?」

他の問題を解きつつ、本問に戻って色々やってみるも結局何が間違っているのか分からず時間切れ。

後日、ほかの正答者に聞いてみたところ、「リトルエンディアンのバイトオーダーで組み立てないと駄目だよ〜」と言われ、「oh…」となりました。

バイトオーダーとはマルチバイトデータをどのようにメモリに格納するかを示すもので、ビッグエンディアンとリトルエンディアンの2種類があります。

ビッグエンディアンは値をそのままの順番でメモリに格納します。例えば01020304という4バイトのデータの場合、メモリ上でも01020304という順番で格納されます。
対して、リトルエンディアンは値を逆順でメモリに格納します。例えば01020304という4バイトのデータの場合、メモリ上では04030201という順番で格納されます。

x86および x86-64環境ではリトルエンディアンが採用されています。
もう一度問題に戻ってみましょう。


mov ds:somedata, 08CCE97ABh
mov ds:somedata+4, 0B9A08CCEh
mov ds:somedata+8, 0FF989E93h

解析対象のバイナリはx86アーキテクチャです。なので、バイトオーダーはリトルエンディアンとなり、DSレジスタの初期値は0x8CCE97ABB9A08CCEFF989E93ではなく、0xAB97CE8CCE8CA0B9939E98FF になります。

以下は、最初のスクリプトを書き直したもの。0xAB97CE8CCE8CA0B9939E98FFをビット反転させます。


#!/usr/bin/env python

# ds 0xAB97CE8CCE8CA0B9939E98FF

word = []

word.append(~0xab & 0xff)
word.append(~0x97 & 0xff)
word.append(~0xce & 0xff)
word.append(~0x8c & 0xff)

word.append(~0xce & 0xff)
word.append(~0x8c & 0xff)
word.append(~0xa0 & 0xff)
word.append(~0xb9 & 0xff)

word.append(~0x93 & 0xff)
word.append(~0x9e & 0xff)
word.append(~0x98 & 0xff)
word.append(~0xff & 0xff)

print(word)

dec = ''

for i in word:

    dec += chr(i)

print(dec)

実行結果。


$ ./solver02.py
[84, 104, 49, 115, 49, 115, 95, 70, 108, 97, 103, 0]
Th1s1s_Flag

正しいFlagはTh1s1s_Flagでした。

ちなみにマルウェア解析者いわく、マルウェア解析時は常にバイトオーダーを頭の片隅においておく必要があるとのこと。例えばマルウェアに外部IPアドレスと通信する機能があるとします。ネットワーク通信のバイトオーダーはビッグエンディアンですが、うっかりデータをリトルエンディアンとして扱った場合、正しいIPアドレスやポート番号が取得できなくなります。

以上。

参考
SECCON実行委員会監修 セキュリティコンテストチャレンジブック P.62-63
Practical Malware Analysis (by Michael Sikorski and Andrew Honig) P.70

Leave a Reply

Your email address will not be published. Required fields are marked *