概要
とある C++ のソフトウェア開発プロジェクトで新人もそろそろ卒業かなという若いイケメンくんが
「先輩!うちのプロジェクトのバイナリー実行ファイルに含まれている変数 mojimoji の値をビルド(翻訳+リンク)した後で書き換えるなんて無茶な事をしたいかもしれないです!!」
そんな状況が発生していたので早起きして業務時間外に Qiita にやり方を書いておくサンタクロース風のシャイなプレゼント方式を取ることにした私だった。(†0)
状況の例
- a.cxx にはビルド後に書き換えたい変数
mojimojiやmojisizeが含まれているとします。
例えば↓こんなのですね。
static volatile constexpr char mojimoji[] = "abcd1234";
static volatile constexpr unsigned char mojisize = sizeof( mojimoji );
# include <iostream>
auto main() -> int
{
std::cout << ( const char* )mojimoji << " " << std::to_string( mojisize );
}
g++ -std=c++14 -O3 a.cxx などして翻訳すると 実行ファイル a (mingw なら a.exe ) ができて、これは実行すると abcd1234 9 と表示してくれます。(†1)
さて、例えばこれを ajipon 9 と表示するように C++ プロジェクト自体のリビルド無しで、ポストプロセスだけでどうにかしたいのです。
方法
確認
nm: シンボルエントリーの確認
はじめに、 a.exe に mojimoji や mojisize のシンボルエントリーがあるか確認してみましょう:
nm -C a.exe | grep moji すると、
00000000004a7018 d mojimoji
00000000004a7010 d mojisize
ありました。(†2)このように出てくる状態なら簡単にどうにかなるでしょう。たぶん。
この表示からは、バイナリーの実行時の仮想メモリー上のアドレス(今回は使いません)、 d なのでデータセクションに居ること、そしてシンボル名が mojimoji などで存在しているとわかります。
objdump: シンボルの位置
セクション情報
objdump -x a.exe | grep -10 Sections: などしてセクション情報を確認しましょう:
pc+0x04: alloc small area: rsp = rsp - 0x28
Handler: 00000000004a6780.
User data:
000: ff ff 01 00
00000000004d4998 (rva: 000d4998): 00000000004a6d40 - 00000000004a6d4a
Version: 1, Flags: none
Nbr codes: 2, Prologue size: 0x04, Frame offset: 0x0, Frame reg: rbp
pc+0x04: FPReg: rbp = rsp + 0x0 (info = 0x0)
pc+0x01: push rbp
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000a5db0 0000000000401000 0000000000401000 00000600 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE, DATA
1 .data 00002f28 00000000004a7000 00000000004a7000 000a6400 2**5
CONTENTS, ALLOC, LOAD, DATA
2 .rdata 0000fbe0 00000000004aa000 00000000004aa000 000a9400 2**6
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .pdata 0000b04c 00000000004ba000 00000000004ba000 000b9000 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .xdata 0000e9a0 00000000004c6000 00000000004c6000 000c4200 2**2
.data セクションはバイナリーファイル上のオフセットアドレス 000a6400 に居るとわかりました。†3
シンボルのアドレス
続いて、 objdump -x a.exe | grep moji などして mojimoji のアドレスオフセットを確認しましょう:
[134](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000018 _ZL8mojimoji
[138](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000010 _ZL8mojisize
_ZL8mojimoji (マングリングされた mojimoji)と _ZL8mojisize (マングリングされた mojisize)がセクションのアドレスオフセット 0x18 と 0x10 にそれぞれ居るとわかります。
ここまでにわかっている事を整理
-
mojimojiは.dataセクションに居る -
.dataセクションはファイルオフセット0x0a6400バイトに居る -
mojimoji( マングリングされた状態だと_ZL8mojimoji) はセクションのオフセット0x18に居る-
mojisize(_ZL8mojisize) はセクションのオフセット0x10に居る
-
と、言うわけで、バイナリーファイル a.exe のオフセットで:
-
0x0a6400+0x18=0x0a6418:mojimojiが居る -
0x0a6400+0x10=0x0a6410:mojisizeが居る
となります。
実際のデータを確認
od -A x -j 0x0a6418 -a a.exe | head -n1 してバイナリーファイル a.exe のファイルオフセットアドレス 0x0a6418 からの mojimoji のバイト列をASCII文字のダンプで確認してみます:
0a6418 a b c d 1 2 3 4 nul nul nul nul nul nul nul nul
居ましたヽ(´ー`)ノ
od -A x -j 0x0a6410 -t d1 a.exe | head -n1 して 0x0a6410 の mojisize を 10進数のダンプで確認してみます:
0a6410 9 0 0 0 0 0 0 0 97 98 99 100 49 50 51 52
居ましたヽ(´ー`)ノ
バイナリーファイルの変数を書き換えて実行
cp a.exe b.exe などしてコピーした b.exe の 0x0a6418 からの 9 bytes を "ajipon\0\0\0" に書き換えて b.exe を実行してみましょう: †4
./b
ajipon 9
できましたヽ(´ー`)ノ
註
- †0: 記事冒頭の概要本文中のどうでもよさそうな表現はフィクションです。
- †1:
volatileを付けないと-O3でシンボルが吹き飛ぶ可能性があります。nmなどでシンボルが見つからない場合の原因となる可能性があります。 - †2:
nmのオプション-Cを付けないと C++ のマングリングされたシンボル名が表示される事になります。複雑な階層構造になっていたりするとひうまんの脳には辛いので C++er はnmの-Cはc++の-std=c++14と同じくらい標準的に付けるものだと思って差し支えありません。 - †3:
grepせずに> a.objdump.txtや| lessなどして読むと出力にちゃんとカラムがそれぞれ何であるか丁寧に出力されていたりします。やり方としての実用上は不要ですが1度は出力全体を眺めておく事をお勧めします。 - †4: コマンドだけでバイナリー差分を書き換えるのは面倒かなと思ったので今回は書き換えに Bz を使いました。