PE形式バイナリ変換ツール
hackbin - PE Binary File Converter
概要
- これまでのあらすじ
- ―― 20xx年、新たなVisualStudioの出現により人類のWindows 2000対応アプリ開発技術は絶滅の危機に瀕していた。人々は失意の中、ある者は富豪を目指してマネージドコードの暗黒面に飮み込まれぱしろへんだすwww(中略)またある者は窓辺ななみ+スク水眼鏡で我慢などといった意味不明な杞憂に耽(中略)そしてある者は青き清浄な世界を求めて過去の汚染されたOSは消毒だなどと叫びながら攻撃色の世紀末モヒカン(中略)などと妄想していたらこんなツールができました。……ハラショー!
VC++2010の足を撃て
ついに、VC++2010ではCランタイムレベルでWindows 2000への互換性がなくなりました。普通なら誰もやらないであろう露骨なOS乗り換え手段を平然とやってのけるMSに痺れます。海外のMSのフォーラムでこの話題がNPC社員の定型文で終わっているのもいとをかし。
とはいえ普段からC++を高級アセンブラとして活用している諸兄諸姉の皆様なら、Cランタイムなどという軟派なコードなぞ触れたこともないでしょうから、こんな話はどこ吹く風といったところでしょう。
しかし今回はリンカの出力レベルでヘッダ情報をXPより前のものに設定できなくなるという罠がありました。仕方なくバイナリエディタを使って手動でシコシコ書き換えるわけですがボクなんだか眠くなってきたよパトラッシュ。というわけでこの自動書き換えツールで死亡フラグ回避です。一方ロシアは他のリンカを使った。
訂正: この記述は間違いです。正しく設定することで、VC++2010でもバージョン情報は変更可能です。hackbinとは何だったのか。
ほかにも、ヘッダをピンポイントで弄って失敗すると一発でゴミファイルになっちゃう素敵機能を追加してあります。気合いで理解できる、覚悟の済んだお友達専用の自己責任ツールです。
以下のような場面で利用可能です。バイナリエディタを使いすぎて筋肉痛になった脳にファイト一発!
- DUMPBINのようなヘッダ情報表示
- 実行ファイルの対応OSバージョン情報等を変更(未対応アプリには絶対に適用しないでください)
- 無駄にサイズが大きく、ヘッダ情報もなんだか微妙におかしい上、あげくの果てにゴミデータが付属している超古代文明の遺産Mark ZbikowskiヘッダことDOSヘッダを書き換え(ヘッダを理論上最短である64バイトまで縮めるとか、巨大な自己解凍型DOSプログラムの偽装に使うとか)
- さすがx64だ、.pdataセクションや.xdataセクションを削除しても何ともないぜ。例外処理なんて当たらなければどうということはない。万が一例外が起きた場合、プロセスがゾンビ化する。いったいおれ どうな て(危険)
- ファイルアライメントを変更して実行ファイルの再構築とかやってみるテスト(危険)
- タイムスタンプ情報など、EDITBINでは書き換えられないヘッダ情報を任意の値に書き換え(危険)
使いかた
実行ファイルをパスの通ったフォルダに置くだけで使えます。レジストリ等は使っていないので不要になったらいつでも削除可能です。
何も指定せずに実行すると以下のように使い方が表示されます。
- PE Binary File Converter v0.07c
- usage: HACKBIN [options] filename [outfile]
- /S name DOS header (S1:auto 2:win32 3:win64 4:compatible)
- /L ver LinkerVersion
- /O ver OperatingSystemVersion
- /B ver SubsystemVersion
- /I ver ImageVersion
- /T num TimeDateStamp
- /C num Characteristics
- /H num DllCharacteristics
- /F num FileAlignment
- /M num CheckSum
- /E sec Erase section
- /R sec Adjust resource
- /N sec Adjust relocation
- /Y Adjust x64 data directory
- /U Adjust import/export table
- /P Adjust entropy
- /A -ys1 x86:-o4b4 x64:-o5.2b5.2
- /X -upa -e.pdata -e.ydata -r.rsrc -n.reloc
- example:
- hackbin unknown.exe
- hackbin -s1 shrink.exe
- hackbin --snatch seed.exe junk.exe
- hackbin -xf512 extreme.exe
ヘッダ情報の表示 (Dump)
ファイル名だけ指定するとヘッダ情報を表示します。
実行ファイルのヘッダレベルでの比較に活用できます。
DOSヘッダを小型化 (Shrink)
- -s<数字>
DOSヘッダを内蔵のものと差し替えて小型化します。
0を指定した場合はヘッダの差し替えを行ないません。
1を指定した場合は実行ファイルによって2または3のヘッダを自動選択します。
2〜3を指定すると理論上最小サイズである64バイトのヘッダになります。
4を指定すると従来と同一のメッセージを表示する128バイトのヘッダとなります。
他の実行ファイルのDOSヘッダに差し替え (Snatch)
- -s <実行ファイル名>
指定されたファイルのDOSヘッダ部分を差し替え対象として使います。
この機能を使うことで、DOSヘッダ部分を自由に変更することができます。例えばDOS時代の巨大アプリなどであっても問答無用で組み込めます。……といっても、もはやOSレベルで無視される領域なので何の意味もないですが、だからこそ敢えて拘ってみるのも面白いかもしれません。
タイムスタンプやチェックサム情報を変更 (Time/suM)
- -t<数値>
実行ファイルのタイムスタンプ情報(大抵はリンカがファイルを作成した時刻が書かれている)を任意の値に変更できます。DLLのExport Directory Tableのタイムスタンプも同時に変更されます。
- -m<数値>
実行ファイルのチェックサム(大抵は0となるが、マニフェストを含めたりするとタイムスタンプに影響を受けた値が入ることがある)を任意の値に変更できます。
異なる環境や時刻に作成されたバイナリを完全に同一の内容にして配布したい、などの用途に活用してください。
サイズの自動調整 (Auto/Xtreme)
- -a
DOSヘッダを縮小しシステム番号を書き換えます。PE64の場合は未使用のデータディレクトリを削除します。
- -x
上記に加え、PE64の構造化例外情報(.pdata .ydata)を削除しリソースデータ(.rsrc)のオフセットを補正しリロケーションテーブル(.reloc)のサイズを補正し、インポートルックアップテーブル内の未使用ポインタを削除し、圧縮率向上のための各種調整を行います。リンク時に/merge:.xdata=.ydataの指定が必要です。
- お約束
これは十分にバイナリの構造を理解している状態で使う特殊なツールです。
内容を把握できる実行ファイルにのみ使用し、一般のアプリには
絶対に使用しないでください。改造が楽しすぎてやめられなくなってしまいます。
設定を間違えた場合、ファイルが復元不可能な状態で破損する危険があります。ヘッダ内容を見て中身がわかる人だけ使ってください。何が起きても知りません。マジ自己責任。
技術解説とかどうでもいい話とか
プログラムの説明はヘッダ書き換えツールとなってますが、やってることはファイル形式コンバータに近いです。ヘッダのサイズが変わると、後続のセクションのオフセット値が全て書き換わるので再計算して書き直してます。
セクションのサイズ計算について。元のセクションのサイズは、SizeOfRawDataから末尾の0の連続回数をスキャンして、そのサイズを差し引いた値を使っています。その後、Misc.VirtualSizeと元のセクションのサイズを比較し、小さい側を基準にRawDataの値の再定義を行なっています。
この処理により、x86のリロケーションセクションなどのような、実体の末尾に0が連続しているセクションは、仮想サイズはそのままにファイル上のサイズだけが縮小されます。また、ファイルアライメントを大きな値に変更した後小さい値に戻すような場合でも、常に同一のイメージが生成されます。副作用としてリンカの出力したオリジナルのバイナリと同一のイメージは永遠に復元できない可能性がありますが実用上まっったく問題ナイデスヨ。
ありえないとは思いますが実サイズより仮想サイズが小さいような特殊なセクションが存在する場合、不要部分は切り捨てられます。警告などは一切表示されません。
セクションサイズ変更/削除に伴い、SizeOfImage値を補正しています。OSはイメージ読み込みの際、SizeOfImageの値と読み込み領域のチェックをしているようで(というかページコミットの過程で必要なのだろうと思われる)、範囲が違っていると弾かれます。
なお、セクションアライメントの値の変更はできません。リンカとかOSのプロセス起動処理を作りなおすレベルの覚悟が必要そうなので。
SizeOfHeaders等の値は間違っていても問題なく動くのですが、気分の問題で補正処理を入れてあります。
ファイルアライメントの値には必ず2の冪乗の値を指定してください。また、必ずセクションアライメントの値以下となるようにしてください。値のエラーチェックは一切しないゆとり仕様なので、変な値を指定すると素直に壊れます。
ファイルアライメントの値を512より小さくするとOS側で認識されなくなります。その場合再度512以上で構築しなおせば元に戻るかもしれません。アライメントなんて4とか16で十分なのに。半分これ目当てで作ったのに。ディスクのセクタサイズの呪縛か、はたまたVMSのページングサイズの怨念か。無念。
オプションは記述順に解釈されます。効果が重複するオプションは最後に記述したものが有効になります。
16進数の数値を記述したい場合は頭に0xをつけてください。
コマンドラインの数値指定や複数のオプションの間の区切りは、空白文字を省略しても適当にノリで解析します。君は空白を正確に記述してもよいし、空白を省略して横着してもよい。
セクション削除(-eオプション)は同時に8つまで指定可能です。それ以外のセクションの操作は1回の実行につき1つだけ行なえます。
インポートテーブル内のインポートルックアップテーブルの省略により大胆なプログラムサイズの縮小が可能となりました。処理内容としては、まずインポートテーブルを全て調査して有効なルックアップテーブルの記録範囲を取得し、該当部分のデータを切り詰めます。そして関連するインポートテーブルおよびインポートアドレステーブルを走査して該当する仮想アドレスを全て書き換えています。
エントロピー補正指定時はコード末尾の0xCCが除去されます。フフフフフフフ怖いか?
改版履歴
- 2010.05.29 v0.01
現実逃避でスモーキーな一発ネタで終わるはずが、なぜか完成してしまったので俺得ツールとして公開。
小人さんがドキュメントを書いている最中にPE32+対応とかオプション増し増しとかいろいろあったが気にしない。
- 2010.05.31 v0.02
処理を書き直し。
.xdataセクションの削除方法を確立。さらなるPE32+バイナリのサイズ縮小に成功。
.rdata/.xdataセクションを完全に削除しても動作可能なバイナリを生成するように改良。これにより実行時に1ページ分メモリ消費量を減らすことが可能。しかし、例外処理が発生しても当局は一切感知しない。あさはかなり……。
- 2010.06.01 v0.02a
リソースRVAの自動調整処理を追加。最近のリソースコンパイラはタイムスタンプを埋め込まないようなのでタイムスタンプ変更機能は没。
さらに実行ファイルのサイズを縮小。やばい、気合いを入れただけ小さくなるのが楽しすぎる。
おまけで64ビット版バイナリも追加。しかしこの手のツールは当面は32ビット版だけで十分かも。
- 2010.06.05 v0.02a
IAT縮小大作戦を断念したのでドキュメントだけ更新。
- 2010.06.14 v0.03
RawDataの末尾が0で埋まっているセクションを切り詰める処理を追加。x86のリロケーション情報のセクションはこれでサイズをだいぶ減らせるはず。
- 2010.06.15 v0.03a
x86版実行ファイルのサイズを10KBまで縮小。v0.01から比べると約2KB削減。MZヘッダ部分をデータとして利用することでさらに512バイト削減できそうだが、ここから先はソースが相当汚くなるため断念。
- 2010.06.16 v0.03b
やっぱり我慢できなくて9.5KBまで縮小しちゃった。てへ。
変換エラー発生時はエラーコード1を返す(成功時は今まで通り0を返す)ように改良。
- 2011.03.03 v0.03c
機能追加しつつ256バイト程度縮小したがファイルサイズに反映されず。無念。
-tオプションでDLLのExport Directory Tableのタイムスタンプも同時に変更するように改良。
- 2011.04.08 v0.03d
機能追加しつつさらに256バイト縮小。ついに9KBまで縮小。
マニフェストつきのバイナリが一致しないので調べてみたら、なぜかチェックサムが書き込まれていて涙目。というわけで-mオプションでチェックサムを設定できるように改良。
- 2011.12.17 v0.04
-yオプション(旧-uオプション)で64ビットバイナリのData Directoryの未使用領域(CLR Runtime Headerと予約領域)を削るように改良。これにより32ビットバイナリと同様に先頭ブロックに5セクション分の情報を書き込めるようになった。
おまけの64ビット版バイナリを削除。
- 2013.08.04 v0.05
あれから三年――
ついにインポートルックアップテーブルの省略を実現。
長き戦いであった。
謎の没ツールhacklibは犠牲になったのだ……。
いろいろ弄ってたのでオプション指定や細かい挙動が昔とはかなり変わってるかも。どうせ誰も使わない俺俺ツールだから問題なし。
- 2013.08.06 v0.05a
インポートルックアップテーブルがインポートテーブルより前に配置されている腐ったバイナリはテーブル省略処理の対象外とする(処理をスキップする)ように改良。該当するのはbinutilsが出力したバイナリ。要するにGCC系は全滅。こんな順序で配置されてしまっては単純なコンバータだけではどうにもならん。
- 2014.06.12 v0.05b
インポートルックアップテーブルを省略した際、テキストセクションの仮想サイズがアライメント境界を超えて縮んだ時、PE32ローダが実行に失敗するため、実サイズのみ弄るよう改良。
- 2014.06.16 v0.05c
セクションの仮想サイズを極力正しく計算するように改良。かつ、テキストセクションの仮想サイズがセクションアライメント境界を超えて縮んでデータセクションの転送位置がずれてPE32ローダがエラーを起こす現象を回避するよう改良。
- 2015.10.31 v0.05d
Windows 10ではWin32 APIを経由しないプロセス終了が動作しなくなった模様。
仕方ないので互換性の下がったWindows 10でも動作するよう改良。
- 2015.11.27 v0.06
今の科学力なら単純な変換魔法でサイズが縮みそうな気がしたので試したらサクっと縮んだ。8.5KB達成でござる。
- 2015.11.29 v0.06a
リロケーションセクションのサイズ補正機能を追加。
- 2015.12.18 v0.06b
ASLRが有効な場合のサイズ下限を4KBに変更。
利用しないオプションを削除してサイズ削減。8KB到達。
- 2017.02.02 v0.07
データ圧縮用途向けにエントロピー補正機能を追加。
- 2017.04.03 v0.07a
DLLの拡張子を除去するよう改良。
- 2017.05.20 v0.07b
DLLの拡張子に大文字も許容するよう改良。
ASLRが無効な場合に末尾の0を除去するよう改良。
Windows8以降の拡張属性と未公開の属性を追加。
- 2017.05.29 v0.07c
コードサイズが8KB以下になるよう再調整。
以下書き途中メモ
書き換えの際、バックアップを取るなんて軟弱なことはしていないので下手に書き換えてファイル破損したらもう二度と復元は不可能。覚悟。
-sによる吸い出し対象のファイルはあまりきちんとチェックしていない。吸い出しに失敗した時はエラーとなる。
たまにLoaderFlagsに0以外を書き込むリンカがある……?ヒャァがまんできねぇ!と思って書き換えたらヘッダが違っててPE32+だった。なんという死亡フラグ
出力ファイル名を省略した場合は指定ファイルをそのまま書き換え。書き換え手順は入力ファイルを読み込みながら書き換えイメージをメモリ上に作成→出力ファイルが既に存在していれば読み込んでベリファイして変更箇所があるか確認→出力ファイルを書き換えモードで再度オープンして出力の順。タイムスタンプ保存とかバックアップ処理はなし。対象が実行中の場合は書き換えできない。
DirectoryデータはDLLでもない限り誰も見てないっぽいので削除できるようにするべき?
セクションヘッダのPointerToRelocationsの補正処理は未実装。どれも0みたいだからこのままでいいや、みたいな。
xdataセクションはリンカ内部でrdataにマージされてしまうため、素の状態では削除不可能。リンク時に/merge:.xdata=.ydataを指定すると内部のマージを無効化できるので、このバイナリに対して-aオプションを適用すればよい。以下の記述を参照。(リンカでワーニングが出るが我慢)
-aオプション適用時にIMAGE_DLLCHARACTERISTICS_NO_SEHを立てておくべきか?現在は何も変更しない。
- #ifndef _M_IX86
- #pragma comment(linker, "/merge:.xdata=.ydata")
- #endif // _M_IX86
セクション実データが終了した後にくっついている署名などのゴミデータをどうするべきか?現在はコピーせず切り捨てている。
プログラム小型化のポイント
微妙なサイズの時はセクションをまとめる(.rdataと.textとか基本)。心配性なら唾(DEP)つけときゃ問題なし
x86ならASLRのセキュリティを犠牲にリロケーション情報をばっさり削除してターンエンド
例外処理なんてモダンなGOTOは排斥して俺によし。ちなみに関数内GOTOはうまく使うとサイズをかなり小さくできる。GOTO最高。
1つの関数内でスタックを4KB以上掘ると邪悪なコードが追加されるので#pragma check_stackとか←すまぬ……邪悪じゃなかった。Windowsの仕様でスタックは4KBづつ連続してコミットしていかないといけない(x86カルトクイズより)。最初から使う量が判る場合は事前にコミットしておけばいいかも(再帰でもしない限り、自前の処理での消費量は簡単に推測できる)。でも、システムコールが将来に渡ってどのくらい消費するのかわからないので、無駄を承知で大目に予約するしかない。実際、スタックを64KBとかに制限するとWin7で落ちるプログラムが出はじめる。どこで落ちてるか追いかけておくべきか……。
intrinsicの暗黒面に身を委ねすぎない……などと中二病風に呟きつつ__movs*とか__stos*を多用する。エンディアン依存コードと同程度の後ろめたさしか感じなくなれば訓練完了。コード小型化とか言う時点で全てを捨てているので何だが、正直近頃のプロセッサじゃ実行速度なんてもはや気にする必要がねえ……。
VC++では伝統的にサイズ不明のmemcpyはintrinsicでも絶対に展開されない、strlenなどはintrinsicにすると自前で組んだ時より逆にサイズが増えることがある、みたいな経験則も大事。
Win32ではグローバル変数は必ず0で初期化されることを利用してコンストラクタを手抜き記述
new/delete置き換えがアツい。それほどでもない
VC++2010は共用体で複数種類のポインタをまとめて使うと、サイズ最適化がいまいちになる場合あり。コードが腐ってる時は別々のポインタにしたりマクロなどでリキャストして使ったほうがいいかも
x86だとサイズ最小化時にFASTCALL宣言してる関数なのにスタック渡しを多用して呼び出されるコードになることが多い。何度も呼び出される関数は、徹底的に引数の数を減らしたり、同一関数内ならパラメータを変数で準備しておいて呼び出し箇所を1箇所に絞るなどするとかなり小さくできる場合もある。代償としてコードの読みやすさ・理解しやすさを失うが、美しかった頃のコードを #if #endif で囲って残すくらいしか対策はなし。
そして必死にサイズを削ったプログラムがメモリを数百キロバイト単位で無駄にするのを見て少し泣く。
酷い思い出ほど美化される
EXE起動時の仕様(DOS3時代のお約束)
- 1パラグラフ16バイトが8086のセグメントの基本
- CP/Mの呪いでメモリブロック先頭16パラグラフがPSP、その直後にロードモジュールが続く
- DSとESは必ず同じ値となりPSP先頭を指す
- CSオフセットを0にした場合、CSはロードモジュール先頭を指す(つまりDS+16)
内蔵DOSヘッダのx86コード HP200LX(80186/DOS5)実機で動作確認
- 0004 MESSAGE:
- 0004 75 73 65 20 ... DB "use win32", 0DH, 0AH, "$"
- 0010 START:
- 0010 BA 04 01 MOV DX, MESSAGE + 100H
- 0013 B4 09 MOV AH, 09H
- 0015 CD 21 INT 21H ; DOS _CPM_PRINT (終了時必ずAL=24Hになる)
- 0017 B8 01 4C MOV AX, 4C01H
- 001A CD 21 INT 21H ; DOS _EXIT2
先頭+2〜3バイトの値はヘッダ部分のサイズと一致させておくべき
先頭+20H〜+23Hの4バイトはシグネチャなので誰でも自由に弄れるように開けておく
シンボルテーブルのオフセット値は0で問題なし(個数0なので)
SP初期値0で問題なし。COM形式ファイルと同じ環境を狙うならさらに0をPUSHしてFFFEにすべきか
やはりINT 20HはWindows XPでは未実装のようです。無念。無理矢理実行してみると例外で止まり、以後仮想86モード自体が不安定になったりします(VMwareで確認したので正しくないかも)。FUNC 4CHで代用するしかないですね。
そういえばSS変更直後の割り込み禁止サイクルで必ずSP書き換えをしないといけないとか懐しいですね。
co (Twitter♺)